I am validating my Bearer token through JWT in Python and it was earlier written in a way to handle idTokens only. We just moved to the new auth code flow pattern on the UI and they advice me to use accessToken instead of idToken for token validation. The app works great E2E if I use idToken, however when I use accessToken in the Bearer auth, the validation fails. I get a 401 unauthorized.
Please advice.
Here is my python code:
import os
import sys
import requests
import time
import calendar
from functools import wraps
from jose import jwk, jwt, JWTError
from flask import abort, current_app, request
from src.database import db
from src.secrets import derive_base64_secret
from src.models.user import User, UserRoleEnum
CLIENT_ID = os.environ.get("AZURE_AD_CLIENT_ID")
TENANT_ID = os.environ.get("AZURE_AD_TENANT_ID")
AUTHORITY_MSAL = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
OIDC = requests.get(f"{AUTHORITY_MSAL}/.well-known/openid-configuration").json()
MSAL_JWKS = requests.get(OIDC["jwks_uri"]).json()
S2S_JWK = {"alg": "HS256", "kty": "oct", "k": derive_base64_secret("S2S JWT", 32)}
def validate_msal_token(token):
try:
return jwt.decode(token, MSAL_JWKS, audience=CLIENT_ID, issuer=[AUTHORITY_MSAL])
except JWTError:
abort(401)
def validate_s2s_token(token):
try:
return jwt.decode(token, S2S_JWK, audience=CLIENT_ID, issuer=CLIENT_ID)
except JWTError:
abort(401)
def create_s2s_token(ttl, additional_claims):
now = calendar.timegm(time.gmtime())
claims = {
"iss": CLIENT_ID,
"aud": CLIENT_ID,
"iat": now,
"exp": now + ttl,
**additional_claims,
}
return jwt.encode(claims, S2S_JWK)
def get_access_token():
authorization = request.headers.get("Authorization")
if isinstance(authorization, str) and authorization.startswith("Bearer "):
return authorization[7:]
return request.args.get("access_token")
def load_user():
access_token = get_access_token()
if not access_token:
abort(401)
token = validate_msal_token(access_token)
if not token:
abort(401)
oid = token.get("oid", None)
if not oid:
abort(403)
user = User.query.filter_by(OID=oid).first()
if not user:
# Auto add the first user to access the API as an Admin IN DEVELOPMENT ONLY
if current_app.env == 'development' and User.query.count() == 0:
user = User(OID=oid, Display_Name="Unknown", User_Name="unknown#example.com", Role=0)
db.session.add(user)
else:
abort(403)
display_name = token.get("name", None)
if display_name and user.Display_Name != display_name:
user.Display_Name = display_name
# `preferred_username` is supplied on a v2 id token as provided by the FM
# `unique_name` is supplied on a v1 id token as provided by the MDA
user_name = token.get("preferred_username", token.get("unique_name", None))
if user_name and user.User_Name != user_name:
user.User_Name = user_name
db.session.commit()
request.token = token
request.user = user
return user
def jwt_required(fn):
#wraps(fn)
def wrapper(*args, **kwargs):
access_token = get_access_token()
if not access_token:
abort(401)
token = validate_s2s_token(access_token)
if not token:
abort(401)
request.token = token
return fn(*args, **kwargs)
return wrapper
def user_required(fn):
#wraps(fn)
def wrapper(*args, **kwargs):
load_user()
return fn(*args, **kwargs)
return wrapper
Thank you for sharing your code. You should receive a response from the bearer auth and be granted access. It appears that your credentials are insufficient for successful authentication resulting in a 401 unauthorized. Based on the situation, perform a secondary check on the cause of failure. Token validation is not required for all apps. Apps should only validate a token in the following circumstances listed here. APIs and web apps can only validate tokens with an aud claim that matches their app; other resources may have their own token validation requirements. Learn more about managing access tokens here.
Related
I tried to make custom auth class (DEFAULT_AUTHENTICATION_CLASSES), I want to auth with Bearer token. I could register an user, but I cannot login as an auth user.
I'm stuck. I used example and now I cannot get some info about auth token
authentication module
class BearerTokenAuthentication(BaseAuthentication):
keyword = 'Bearer'
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != self.keyword.lower().encode():
return None
try:
access_token = auth[-1]
payload = jwt.decode(
access_token, settings.SECRET_KEY, algorithms=["HS256"]
)
except jwt.ExpiredSignatureError:
raise exceptions.AuthenticationFailed("The access token expired")
except IndexError:
raise exceptions.AuthenticationFailed("Bearer prefix missing")
return self.authenticate_credentials(payload["id"])
def authenticate_credentials(self, user_id: str):
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed("Invalid token.")
return user, None
I guess I missed some settings because the auth variable is None. I wrote in the settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'custom_authentication.authentication.BearerTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
}
AUTH_USER_MODEL = 'users.User'
serializer module
class LoginSerializer(serializers.ModelSerializer):
email = serializers.EmailField(max_length=255)
password = serializers.CharField(max_length=68, min_length=6, write_only=True)
confirm_password = serializers.CharField(write_only=True)
access = serializers.CharField(max_length=555, min_length=1, read_only=True)
refresh = serializers.CharField(max_length=555, min_length=1, read_only=True)
class Meta:
model = User
fields = ["id", "email", "password", "confirm_password", "access", "refresh"]
def validate(self, attrs):
email = attrs.get("email", "")
password = attrs.get("password", "")
confirm_password = attrs.get("confirm_password", "")
if password != confirm_password:
raise exceptions.ValidationError("Those password don't match")
validate_password(password)
user = auth.authenticate(email=email, password=password)
if not user:
raise AuthenticationFailed("Invalid credentials, try it again")
return {
"id": user.id,
"email": user.email,
"access": user.get_access().get("access"),
"refresh": user.get_refresh().get("refresh"),
}
Now I'm getting this "Invalid credentials, try it again". The user variable is None, too
What I'm doing?
I have a requirement where there will be separate authorisation server and a resource server.
I am using Resource owner password based grant type.
I have implemented Custom Introspection View and Get Token View. (Following this and this: I have used the DOT's base class to implement my own introspection API, will add it at the end.)
For both Custom Views I created, I have added it to the urls.py manually referring to it.
What's happening now?
Turns out, you gotta refresh token using the same /token/ url which you used to get a access token. The only difference is you gotta change the grant type to refresh_token when you wanna refresh token.
It does not get processed as I have not implemented or am able to implement the refresh token. I am happy with the pre-existing internal refresh token mechanism as I don't require special/custom stuff to be in it but as I'm using a custom token view, grand_type = refresh_token is not hitting the oauthlibs refresh_token view. (My observation)
urls.py
from django.contrib import admin
from django.urls import path, include
from project.apis import (
ClientCreateTokenView,
ResourceServerIntrospectView
)
urlpatterns = [
path('admin/', admin.site.urls),
path('o/token/', ClientCreateTokenView.as_view(), name='token_obtain_pair'),
path('o/introspect/', ResourceServerIntrospectView.as_view(), name='introspect_token'),
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]
apis.py
from django.http import HttpResponse
from oauth2_provider.views.base import TokenView
from oauth2_provider.views import IntrospectTokenView
from django.utils.decorators import method_decorator
from django.views.decorators.debug import sensitive_post_parameters
from oauth2_provider.models import get_access_token_model
from oauth2_provider.signals import app_authorized
import json
from iam_management.models import AuthorisedClients
from rest_framework import status
from base64 import b64decode
from django.contrib.auth.models import User
import time
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse
import calendar
class ResourceServerIntrospectView(IntrospectTokenView):
"""
Implements an endpoint for token introspection based
on RFC 7662 https://tools.ietf.org/html/rfc7662
"""
#staticmethod
def get_token_response(token_value=None, resource_server_app_name=None):
try:
token = (
get_access_token_model().objects.select_related("user", "application").get(token=token_value)
)
except ObjectDoesNotExist:
return JsonResponse({"active": False}, status=200)
else:
authorised_clients = AuthorisedClients.objects.filter(user=token.user).first()
if token.is_valid() and resource_server_app_name in json.loads(authorised_clients.authorised_apps):
data = {
"active": True,
"exp": int(calendar.timegm(token.expires.timetuple())),
}
if token.application:
data["client_id"] = token.application.client_id
if token.user:
data["username"] = token.user.get_username()
return JsonResponse(data)
else:
return JsonResponse({"active": False}, status=200)
def get(self, request, *args, **kwargs):
return HttpResponse(status=405)
def post(self, request, *args, **kwargs):
"""
Get the token from the body parameters.
Body: { "token": "mF_9.B5f-4.1JqM" }
"""
if "token" in request.POST and "app_name" in request.POST:
return self.get_token_response(request.POST["token"], request.POST["app_name"])
else:
return HttpResponse(status=400)
class ClientCreateTokenView(TokenView):
#method_decorator(sensitive_post_parameters("password"))
def post(self, request, *args, **kwargs):
try:
username = request.POST["username"]
app_name = request.POST["app_name"]
client_id = \
b64decode(request.META["HTTP_AUTHORIZATION"].split(" ")[1].encode("utf-8")).decode("utf-8").split(":")[
0]
except:
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
authorized_client = AuthorisedClients.objects.filter(user=User.objects.get(username=username),
client_id=client_id).first()
if not authorized_client:
return HttpResponse(status=status.HTTP_401_UNAUTHORIZED)
authorised_apps = json.loads(authorized_client.authorised_apps)
if app_name.lower() not in authorised_apps:
return HttpResponse(status=status.HTTP_401_UNAUTHORIZED)
url, headers, body, return_status = self.create_token_response(request)
if return_status == 200:
body = json.loads(body)
access_token = body.get("access_token")
if access_token is not None:
token = get_access_token_model().objects.get(token=access_token)
app_authorized.send(
sender=self,
request=request,
token=token
)
if "scope" in body.keys():
del body["scope"]
body = json.dumps(body)
response = HttpResponse(content=body, status=return_status)
for k, v in headers.items():
response[k] = v
return response
I know that I am not really following the standards and putting custom stuff here for more checks but that's just my use case. I am unable to find anything on refresh token on DOT even though it is a famous and maintained package.
I have this main.py file. I am creating a jwt token here at /auth endpoint. After the token is generated, now I am unable to redirect it to base path("/"). How can I achieve that. If I try to access the / path, i get redirected to auth endpoint with the bearer token displayed. Any help or pointers on how this can be done.
main.py
from authlib.integrations.starlette_client import OAuth
oauth = OAuth()
CONF_URL = "https://localhost:8080/.well-known/openid-configuration"
oauth.register(
name="cad",
server_metadata_url=CONF_URL,
client_id=settings.CLIENT_ID,
client_secret=settings.CLIENT_SECRET,
client_kwargs={"scope": "openid email profile authorization_group"},
)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/auth')
def create_access_token(*, data: dict, expires_delta: datetime.timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.datetime.utcnow() + expires_delta
else:
expire = datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
to_encode.update({'exp': expire})
encoded_jwt = jwt.encode(to_encode, "abcd", algorithm="HS256")
return encoded_jwt
def create_token(id):
access_token_expires = datetime.timedelta(minutes=120)
access_token = create_access_token(data={'sub': id}, expires_delta=access_token_expires)
return access_token
#app.middleware("http")
async def authorize(request: Request, call_next):
if not (request.scope["path"].startswith("/login") or request.scope["path"].startswith("/auth")):
if not is_session_okay(request.session):
return RedirectResponse(url="/login")
return await call_next(request)
#app.get("/login")
async def login(request: Request):
redirect_uri = request.url_for("auth")
return await oauth.cad.authorize_redirect(request, redirect_uri)
#app.get("/auth")
async def auth(request: Request):
try:
token = await oauth.cad.authorize_access_token(request)
except OAuthError as error:
return HTMLResponse(f"<h1>{error.error}</h1>")
user = await oauth.cad.parse_id_token(request, token)
access_token = create_token(user['sub'])
return {"access_token": access_token, "token_type": "bearer"}
#app.get("/", tags=["Web-UI"])
def index():
frontend_root = "./ui"
return FileResponse(str(frontend_root) + "/index.html", media_type="text/html")
Since you are using fastapi you should use it's jwt implementation.
The framework provide classes especialy for that => OAuth2PasswordBearer and OAuth2PasswordRequestForm
the doc on this is available in the advanced documentation:
(from the doc)
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
class Token(BaseModel):
access_token: str
token_type: str
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
#app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
Building Microservices With Django and pyJWT
Let me explain this in summarised manor, maybe someone can help me out been at this for too long
I have three microservices i.e.
Authentication : Which creates the User and makes the token for a user using RS256 algorithm
Student : which is basically a user type that needs to be verified using JWT
Investor : which is also a user type that needs to be verified using JWT
[I have tried to make a generic create token function in authentication service something like]
data = {"id": user.uuid.hex, "type": user.user_type}
encoded = jwt.encode(data, private_key, algorithm="RS256")
return encoded
It is generating a correct token i have verified using JWT.io
In my student service i have created a middleware something like this
class JWTAuthenticationMiddleware(object):
#Simple JWT token based authentication.
#Clients should authenticate by passing the token key in the "Authorization"
#HTTP header, prepended with the string "Bearer ". For example:
# Authorization: Bearer <UNIQUE_TOKEN>
keyword = "Bearer"
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
auth = request.META.get("AUTHORIZATION", None)
splitted = auth.split() if auth else []
if len(splitted) == 2 and splitted[0] == self.keyword:
if not splitted or splitted[0].lower() != self.keyword.lower().encode():
return None
if len(splitted) == 0:
msg = _("Invalid token header. No credentials provided.")
raise exceptions.AuthenticationFailed(msg)
elif len(splitted) > 2:
msg = _("Invalid token header. Token string should not contain spaces.")
raise exceptions.AuthenticationFailed(msg)
user = get_user(request)
decoded = verify_token(auth)
try:
student = Student.objects.get(user_id=decoded.get("id"))
except Student.DoesNotExist:
student = Student.objects.update(user_id=decoded.get("id"))
response = self.get_response(request)
return response
My verify_token function looks like
def verify_token(token):
decoded = jwt.decode(token, JWT_AUTH["PUBLIC_KEY"], algorithms=["RS256"])
return decoded
I have also added my middleware in my settings just below authentication middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"config.middleware.authentication.JWTAuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.common.BrokenLinkEmailsMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
I am just not able to decode my token and assign the payload to respective user type, Can someone help me out where i am going wrong? I have also tried using Authentication backend something like this, it doesn't works, have implemented simple_jwt_djangorestframework package but it doesn't decode my payload on student service and says invalid token so i don't want to add it for increasing unnecessary code too.
My viewset looks like this
class StudentViewset(viewsets.ViewSet):
queryset = Student.objects.filter(is_active=True)
serializer_class = StudentSerializer
lookup_field = "uuid"
permission_classes = [IsAuthenticated]
My error is always saying when i am using isAuthenticated as permission class
"message": "Authentication credentials were not provided.",
Maybe validating raw token should work :
decoded = verify_token(splitted[1])
Actually you don't have to implement it. There is simple jwt package for it. Install the package by its documentation and if it wouldn't work provide errors to help you.
To answer this question myself for future references
class JWTAuthenticationMiddleware(object):
#Simple JWT token based authentication.
#Clients should authenticate by passing the token key in the "Authorization"
#HTTP header, prepended with the string "Bearer ". For example:
# Authorization: Bearer <UNIQUE_TOKEN>
keyword = "Bearer"
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
auth = request.META.get("AUTHORIZATION", None)
splitted = auth.split() if auth else []
if len(splitted) == 2 and splitted[0] == self.keyword:
if not splitted or splitted[0].lower() != self.keyword.lower().encode():
return None
if len(splitted) == 0:
msg = _("Invalid token header. No credentials provided.")
return JsonResponse(data=msg, status=403, safe=False)
elif len(splitted) > 2:
msg = _("Invalid token header. Token string should not contain spaces.")
return JsonResponse(data=msg, status=403, safe=False)
user = get_user(request)
decoded = verify_token(auth)
try:
student = Student.objects.get(user_id=decoded.get("id"))
return self.get_response(request)
except Student.DoesNotExist:
msg = _("Invalid User Not found")
return JsonResponse(data=msg, status=403, safe=False)
couple of things that i was doing wrong was not passing a response when i found a student that's why middleware was not bypassed at that time, also exceptions cannot be used at middleware level middleware only returns HTTPResponses, thus i thought of using JsonResponse.
The use case
I am trying to connect to Microsoft Dynamics 365 - Field Service.
I am using Python, Falsk and OAuth2Session to perform a Oauth2 authentication
I have setup the Azure App on Azure.
the error message
I keep receiving the HTTP Error 401
Who could help me?
the code : config.py
"""Configuration settings for running the Python auth samples locally.
In a production deployment, this information should be saved in a database or
other secure storage mechanism.
"""
import os
from dotenv import load_dotenv
load_dotenv()
CLIENT_ID = os.environ["dynamics365_field_service_application_client_id"]
CLIENT_SECRET = os.environ["dynamics365_field_service_client_secrets"]
REDIRECT_URI = os.environ["dynamics365_field_service_redirect_path"]
# AUTHORITY_URL ending determines type of account that can be authenticated:
# /organizations = organizational accounts only
# /consumers = MSAs only (Microsoft Accounts - Live.com, Hotmail.com, etc.)
# /common = allow both types of accounts
AUTHORITY_URL = os.environ["dynamics365_field_service_authority"]
AUTHORIZATION_BASE_URL = os.environ["dynamics365_field_service_authorization_base_url"]
TOKEN_URL = os.environ["dynamics365_field_service_token_url"]
AUTH_ENDPOINT = "/oauth2/v2.0/authorize"
RESOURCE = "https://graph.microsoft.com/"
API_VERSION = os.environ["dynamics365_field_service_version"]
SCOPES = [
"https://admin.services.crm.dynamics.com/user_impersonation"
]
# "https://dynamics.microsoft.com/business-central/overview/user_impersonation",
# "https://graph.microsoft.com/email",
# "https://graph.microsoft.com/offline_access",
# "https://graph.microsoft.com/openid",
# "https://graph.microsoft.com/profile",
# "https://graph.microsoft.com/User.Read",
# "https://graph.microsoft.com/User.ReadBasic.All"
# ] # Add other scopes/permissions as needed.
the code : dynamics365_flask_oauth2.py
# *-* coding:utf-8 *-*
# See https://requests-oauthlib.readthedocs.io/en/latest/index.html
from requests_oauthlib import OAuth2Session
from flask import Flask, request, redirect, session, url_for
from flask.json import jsonify
import os
import flaskr.library.dynamics365.field_service.config as config
app = Flask(__name__)
app.secret_key = os.urandom(24)
# This information is obtained upon registration of a new dynamics
# client_id = "<your client key>"
# client_secret = "<your client secret>"
# authorization_base_url = 'https://dynamics.com/login/oauth/authorize'
# token_url = 'https://dynamics.com/login/oauth/access_token'
#app.route("/")
def index():
"""Step 1: User Authorization.
Redirect the user/resource owner to the OAuth provider (i.e. dynamics)
using an URL with a few key OAuth parameters.
"""
dynamics = OAuth2Session(
config.CLIENT_ID, scope=config.SCOPES, redirect_uri=config.REDIRECT_URI
)
authorization_url, state = dynamics.authorization_url(config.AUTHORIZATION_BASE_URL)
# State is used to prevent CSRF, keep this for later.
session["oauth_state"] = state
print(f"Please go here and authorize : {authorization_url}")
return redirect(authorization_url)
# Step 2: User authorization, this happens on the provider.
#app.route("/login/authorized", methods=["GET"]) # callback
def callback():
""" Step 3: Retrieving an access token.
The user has been redirected back from the provider to your registered
callback URL. With this redirection comes an authorization code included
in the redirect URL. We will use that to obtain an access token.
"""
if session.get("oauth_state") is None:
return redirect(url_for(".index"))
dynamics = OAuth2Session(
config.CLIENT_ID, state=session["oauth_state"], redirect_uri=config.REDIRECT_URI
)
token = dynamics.fetch_token(
token_url=config.TOKEN_URL,
client_secret=config.CLIENT_SECRET,
authorization_response=request.url,
)
print(f"token: {token}")
# At this point you can fetch protected resources but lets save
# the token and show how this is done from a persisted token
# in /profile.
session["oauth_token"] = token
# return redirect(url_for(".dynamics_get_accounts_postman"))
return redirect(url_for(".dynamics_get_accounts"))
#app.route("/profile", methods=["GET"])
def profile():
"""Fetching a protected resource using an OAuth 2 token.
"""
dynamics = OAuth2Session(config.CLIENT_ID, token=session["oauth_token"])
return jsonify(dynamics.get("https://api.dynamics.com/user").json())
#app.route("/get_accounts")
def dynamics_get_accounts():
if session.get("oauth_token") is None:
return redirect(url_for(".index"))
dynamics = OAuth2Session(
client_id=config.CLIENT_ID,
# token="Bearer " + session["oauth_token"]["access_token"]
token=session["oauth_token"],
)
result = dynamics.get("https://{env_name}.{region}.dynamics.com/api/data/v9.0")
if result.status_code != 200:
result = {"status code": result.status_code, "reason": result.reason}
else:
result = result.json()
result = jsonify(result)
return result
import requests
#app.route("/dynamics_get_accounts_postman")
def dynamics_get_accounts_postman():
if session.get("oauth_token") is None:
return redirect(url_for(".index"))
url = "https://{env_name}.{region}.dynamics.com/api/data/v9.0/accounts"
payload = {}
headers = {
"Accept": "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"If-None-Match": "null",
"Authorization": f'Bearer {session["oauth_token"]["access_token"]}',
}
response = requests.request("GET", url, headers=headers, data=payload)
result = response.text.encode("utf8")
print(f"result : {result}")
return jsonify(result)
if __name__ == "__main__":
# This allows us to use a plain HTTP callback
os.environ["DEBUG"] = "1"
app.secret_key = os.urandom(24)
app.run(debug=True)
The parameter resourcewas missing when generating the authorization_url.
authorization_url, state = dynamics.authorization_url(
config.AUTHORIZATION_BASE_URL + f"?resource={config.DYNAMICS365_CRM_ORG}"
)