How to identify requesting keycloak user in Flask app - python

I'm pretty new to the world of authentication / keycloak, and after reading the keycloak doc I still can't wrap my head around the following situation.
Let's assume there's an existing web-application with keycloak as IAM.
Now, I need to implement a Flask/dash application which provides a service with business client specific data and dedicated API endpoints for all the business clients. Let's say the base URL is website.com/myapp/
Each business client has users in keycloak and some, not all, should access the data at the according endpoint, website.com/myapp/client1/
user_1 can view /client1/, user_2 and user_3 can view /client2/
That means I have to hardcode somewhere (database?) which user belongs to a client endpoint, correct?
In keycloak I've setup a client (myapp) with service accounts enabled
I gave the client a role view
I gave a user the role view
With the client_id and client_secret I can obtain the access_token and make all the known requests via the keycloak API.
The API routes in Flask/dash have a decorator function to check if the user is logged in and has the general view permission for myapp.
This works fine, but the problem is I need to pass the user_id to check if the keycloak session is active or not.
How do I check for the logged in user_id or user_name who wants to access /myapp/ so that I can validate the view permission and match to the dedicated Flask route?
authentication:
def authentication(user_id):
url = 'http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/token'
payload = 'grant_type=client_credentials'
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
response = requests.request('POST', url, headers=headers, data=payload, auth=HTTPBasicAuth(client_id, client_secret))
token = json.loads(response.text)
return get_client_roles(token['access_token']), get_sessions(token['access_token'], user_id)
def get_sessions(token, user_id):
url = f'http://127.0.0.1:8080/auth/admin/realms/master/users/{user_id}/sessions'
payload = {}
headers = { 'Authorization': 'Bearer '+token }
response = requests.request('GET', url, headers=headers, data=payload)
sessions = json.loads(response.text)
return sessions
def get_client_roles(token):
url = 'http://127.0.0.1:8080/auth/admin/realms/master/clients/'+id+'/roles/view/users'
payload = {}
headers = { 'Authorization': 'Bearer '+token }
response = requests.request('GET', url, headers=headers, data=payload)
client_roles = json.loads(response.text)
return client_roles
With that I can check if user_x is logged in and has role view for myapp
But unfortunately I need to hardcode the user_id in my Flask/dash file:
excerption from Flask/dash:
dash_app1 = dash.Dash(server = app, name = 'Dashboard client1', url_base_pathname='/client1/')
dash_app2 = dash.Dash(server = app, name = 'Dashboard client2', url_base_pathname='/client2/')
...
user_id = 'dsdgbc-fd68-41c3-b6d-34fds343436aa75' # how to retrieve and not hardcode?
#app.route("/client1/", endpoint='route1')
#login_required
def render_dashboard():
return redirect('/client1/')
The user_id is the parameter in authentication(user_id) in the decorator function

Related

How to handle OAuth flow from VueJS frontend to Flask backend

I have a Flask backend server that integrates Reddit's OAuth integration via PRAW, and a VueJS Frontend.
The backend APIs for handling the OAuth cycle look like this:
#api_user.route('/login', methods=['GET'])
def login() -> Response:
reddit: Reddit = Reddit()
auth_url: str = reddit.auth.url(scopes=SCOPES, state=STATE)
auth_url = urllib.parse.unquote(auth_url) # remove url encodings
return redirect(auth_url)
#api_user.route('/auth_callback', methods=['GET'])
def callback() -> Tuple[Response, int]:
code: str = request.args.get('code', '')
reddit = Reddit()
refresh_token: str = reddit.auth.authorize(code)
me: Redditor = reddit.user.me()
token = jwt.encode({
'refresh_token': refresh_token,
'user': {
'name': me.name,
'id': me.id
}
}, str(SECRET_KEY), algorithm=ALGORITHM)
response: Response = make_response(jsonify({
'token': token
}))
response.set_cookie('access_token', token, max_age=COOKIE_MAX_AGE)
return response, 200
#api_user.route('/me', methods=['GET'])
#jwt_required
def about(reddit: Reddit) -> Tuple[Response, int]:
user: Redditor = reddit.user.me()
return jsonify({
'name': user.name,
'id': user.id,
'karma': user.link_karma + user.comment_karma
}), 201
/api/login redirects to Reddit's authorization URL where user can provide their username/password, which then redirects the callback uri /api/auth_callback (set in the Reddit app configuration), which then generates a refresh token and sends it as a JWT Token to be stored as a cookie.
What I would like to know is how would I connect this flow with a separate VueJS frontend. Even if the VueJS login page /login calls the API /api/login, after the OAuth is finished it will end up at the server-side route /api/auth_callback?code=xyz.
Integrating VueJS in the Jinja2 Template and making the whole application contained within Flask can work, but it becomes messy quickly.
Another way is to make the redirect_uri be a VueJS route, not a backend route. But in that case, I would like to know how can I parse the URL and extract the code parameter (like I did in the /auth_callback Flask route handler). In that case, I can get rid of /api/auth_callback and add /api/jwt which takes the code parameter extracted by the Vue frontend and returns the JWT token after validating using PRAW.
Any kind of help would be appreciated. Thanks.

How to create a request object in Django with a session and cookies of an authenticated user?

In a Django view I can use the cookies from the incoming request and pass it into another request as means of authentication and make requests to other views in the project using the requests module.
def someview(request):
cookies = request.COOKIES
csrftoken = cookies['csrftoken']
baseurl = settings.BASEURL
url = f"{baseurl}/api/csvuploads/"
payload = {...}
resp = requests.post(
url,
data=payload,
cookies=cookies,
headers={'X-CSRFToken': csrftoken},
)
I need to have a management command running periodically (as a cronjob) to have a similar logic, the problem is that the management command will run in a container in Kuberentes so I need to create an user, a request, authenticate, and login so I can make more requests.
How can I recreate the same request with session, cookies and everything if the request is not coming from a view?
My first attempt was to use RequestFactory from the test framework but I couldn't figure out how to create the request with the session and cookies from the authentication.
def handle(self, *args, **options):
factory = RequestFactory()
request = factory.post(
"/admin/login",
{'username': username, 'password': password}
)
user = authenticate(request, username=username, password=password)
request.user = user
I know I could do this with the test Client() but would it be a good practice to use a testing tool to make requests in a production-like script?

Django verify auth from request

I am interested in having the user send a request in this fashion, using requests:
import requests
url = "https://postman-echo.com/basic-auth"
username = "postman"
password = "password"
response = requests.get(url, auth=(username, password))
Then in my Django model I need to be able to verify that those auth credentials are matching with the right user.
I cannot access request.auth and there are no auth fields in request.META.
What is the right way to access those credentials from the Django side?
I found the answer here.
auth_header = request.META['HTTP_AUTHORIZATION']
encoded_credentials = auth_header.split(' ')[1] # Removes "Basic " to isolate credentials
decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8").split(':')

Set up authentication/authorization with flask-jwt-extended + ariadne (graphql) + react

I'm trying to create an auth system for my react(nextjs) app with flask_JWT_extended + ariadne(graphql). I have succesfully set up the login mutation that retrieves the access and refresh token but I dont know how to properly integrate them into my app. I am aware the access token is used to make subsequent requests and the refresh token is used to maintain token freshness but I dont know how to implement it with this stack.
mutations.py
Here is my login mutation that returns the access_token and refresh_token. It works fine.
#mutation.field("login")
#convert_kwargs_to_snake_case
def resolve_login(_, info, username, password):
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
payload = {
"user": user,
"access_token": access_token,
"refresh_token": refresh_token,
}
return payload
core.py
Here are my JWT configs, from what I have gathered online I am supposed to do a check on the token on each api request to maintain its freshness but I dont know how to do that especially with python + ariadne. Here is a link with someone implementing it with nodejs: https://github.com/benawad/graphql-express-template/blob/22_advanced_jwt_auth/auth.js
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://localhost/pinkle"
app.config["JWT_SECRET_KEY"] = "this_is_a_secret"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
JWTManager(app)
index.js
Here is my front end making the call to login the user, it returns the tokens but I dont know where to utilize the tokens or if I should save it in client side state and just make calls with the token.
function login({ username, password }) {
axios
.post('http://localhost:5000/graphql', {
query: `mutation {
login(username: "${username}", password: "${password}") {
user {
id
username
password
}
}
}`,
})
.then(result => {
console.log(result.data)
})
}
The Flask-JWT-Extended documentation includes examples of utilizing JWTs from JavaScript which might be helpful for you: https://flask-jwt-extended.readthedocs.io/en/stable/token_locations/

Unauthorized response to POST request in Django Rest Framework with JWT Token

I am building a REST API with Django Rest Framework. I currently have an issue where some of my endpoints return HTTP 401 Unauthorized, whereas the vast majority of my endpoints return correct responses. For authentication I am using JWT tokens with djangorestframework-simplejwt.
I've configured Django to use token auth with djangorestframework-simplejwt.
# rest framework config settings
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
# 'rest_framework.permissions.AllowAny',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
The vast majority of my endpoints return valid data when I pass a valid access token in the request. If I do not send a valid token, I receive a HTTP 403.
On the other hand, I have some custom API views which return a HTTP 401 regardless of whether I pass a valid token or not.
I have included the code to one of my problematic views below.
class CheckDifferentialView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
authentication_classes = [TokenAuthentication]
serializer_class = QuizDifferentialSerializer
def post(self, request, *args, **kwargs):
"""
A function to check a quiz question and to update a user's record of questions answered
"""
print(request.META)
if 'answer' not in request.data:
return JsonResponse({'Error': 'answer not found in request'}, status=status.HTTP_400_BAD_REQUEST)
answer = get_object_or_404(Differential, pk=request.data['answer'])
serializer = QuizDifferentialSerializer(answer)
if answer.is_correct:
pass
# record correct results
else:
pass
# record failed result
return Response(serializer.data, status=status.HTTP_200_OK)
And here is my script which I am using to test my API
import requests
import json
POST_LOGIN_URL = 'http://localhost:8000/api/v1/token/'
POST_URL= 'http://localhost:8000/api/v1/check_differential'
REQUEST_URL = 'http://localhost:8000/api/v1/users'
with requests.Session() as session:
post = session.post(POST_LOGIN_URL, json={"username": "j", "monkey": "aphextwin21"})
token = json.loads(post.text)['access']
headers = {'Authorization': 'Bearer ' + token}
r = session.post(POST_URL, headers=headers, json={"answer": "2"})
# r = session.get(REQUEST_URL, headers=headers)
print(token)
print(r.text, r.status_code)
The desired behaviour is that if I send a POST request with a valid token to this endpoint that is would authorise me and carry on with its day. If no Authorization header with a valid access token is given, then I expect it to deny the request.
Update
Enthusiastic Martin kindly point out that
authentication_classes = [TokenAuthentication]
Was overriding the defaults found in my settings file. I was not aware that as far as Django is concerned TokenAuthentication and JWTAuthentication are treated differently. Now I know.
After removing the authentication_classess = [TokenAuthentication] from my views, the views are working as they should.
The view's authentication class is explicitly set to TokenAuthentication only. It wont work with JWT token.
authentication_classes = [TokenAuthentication]
You either remove this to let the default classes handle it or change it to accept JWTAuthentication.

Categories