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

Fix/keycloak timeout #445

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

Fix/keycloak timeout #445

wants to merge 12 commits into from

Conversation

MichalJura
Copy link
Collaborator

Your checklist for this pull request

  • [ x] I've read the contributing guideline.
  • [ x] I've tested my changes by building and running mquery, and testing changed functionality (if applicable)
  • I've added automated tests for my change (if applicable, optional)
  • I've updated documentation to reflect my change (if applicable)

What is the current behaviour?

Currently mquery does not revoke token by itself therefore forcing relogin.
Also after token expiration user is forced to log in even if instance allows anonymous access,
What is the new behaviour?

Test plan

Token is auto-refreshing itself now.
Also when the token expires, user get anonymous access.
Also User is not logged out so often.

Closing issues

fixes #411
fixes #425

@MichalJura MichalJura requested a review from msm-cert January 13, 2025 22:01
"client_id": db.config.openid_client_id,
"client_secret": db.config.openid_secret,
}
url = "http://mquery-keycloak-1:8080/auth/realms/myrealm/protocol/openid-connect/token"
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't be hardcoded - use url from config instead

@@ -70,6 +74,24 @@ def with_plugins() -> Iterable[PluginManager]:
plugins.cleanup()


def get_new_tokens(refresh_token):
Copy link
Member

Choose a reason for hiding this comment

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

Please add typing (I guess get_new_tokens(refresh_token: str) -> Tuple[str, str], but recheck)

Copy link
Member

@msm-cert msm-cert left a comment

Choose a reason for hiding this comment

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

Ping about hardcoded URL and style nitpicks.

I feel verifying OIDC correctness here is out of my depth, so I asked @psrok1 to help with the review of this aspect.

@@ -1,6 +1,8 @@
from contextlib import asynccontextmanager
import os

import requests # type: ignore

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

(I accidentaly removed this comment now, but it still applies - unnecessary empty line.

Copy link
Member

@msm-cert msm-cert left a comment

Choose a reason for hiding this comment

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

I did a more in-depth review and found a few more stylistic problems. Also a few design comments (especially cookie usage).

const rawToken = localStorage.getItem("rawToken");
if (rawToken) {
const expiresAt = localStorage.getItem("expiresAt");
if (Date.now() > expiresAt) {
Copy link
Member

Choose a reason for hiding this comment

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

Probably it would be better to check if token expires in the next ~30 seconds? To avoid situations where token is 0.1 second before expiration during the check, but server gets an expired token already. That, and clock desync.


export const tokenExpired = () => {
const rawToken = localStorage.getItem("rawToken");
if (rawToken) {
Copy link
Member

Choose a reason for hiding this comment

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

nitpick - if possible prefer early exit and avoid nested ifs. So please change the flow to

if (!rawToken) {
    return false;
}

if (response.data["token"]) {
storeTokenData(response.data["token"]);
} else {
return;
Copy link
Member

Choose a reason for hiding this comment

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

Does this return here do anything? If not, please remove it.

const response = await axios.request("/api/token/refresh", {
method: "POST",
headers: headers,
withCredentials: true,
Copy link
Member

Choose a reason for hiding this comment

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

Why is withCredentials here necessary? If it's not needed, please remove it.

Copy link
Member

Choose a reason for hiding this comment

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

Nevermind, I just noticed the code uses cookies.

if (location_href) {
window.location.href = location_href;
} else {
window.location.href = "/";
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: this is technically incorrect, since mquery doesn't have to be hosted at / (it won't work with, for example, a web proxy that serves mquery on some.website.pl/mquery/). But I think this problem is safe to ignore, just wanted to point that out.

const login = async (token_data) => {
token_data.not_before_policy = token_data["not-before-policy"];
delete token_data["not-before-policy"];
const response = await api.post("/login", token_data);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const response = await api.post("/login", token_data);
await api.post("/login", token_data);

since response is not used anywhere

@@ -14,6 +14,7 @@ class ConfigPage extends Component {
}

componentDidMount() {
localStorage.setItem("currentLocation", window.location.href);
Copy link
Member

Choose a reason for hiding this comment

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

Are you sure there is no way to solve this in a more generic way? Adding the code to set a variable on every subpage feels suboptimal and error-prone.

export const storeTokenData = (token) => {
localStorage.setItem("rawToken", token);
const decodedToken = parseJWT(token);
localStorage.setItem("expiresAt", decodedToken.exp * 1000);
Copy link
Member

Choose a reason for hiding this comment

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

Please don't keep this in localStorage. It's better to avoid having two separate variables when one is necessary. Instead parse rawToken when necessary.


export const refreshAccesToken = async () => {
const rawToken = localStorage.getItem("rawToken");
const expiresAt = localStorage.getItem("expiresAt");
Copy link
Member

Choose a reason for hiding this comment

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

unused

@app.post("/api/login", response_model=LoginSchema, tags=["stable"])
async def login(request: Request, response: Response) -> LoginSchema:
token = await request.json()
if token["refresh_token"]:
Copy link
Member

Choose a reason for hiding this comment

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

The code intentionally doesn't use cookies and passes all parameters by the API. Please change this to keep refresh_token in local storage instead of adding a cookie.

@msm-cert
Copy link
Member

(also please rebase this onto master, since there are merge conflicts)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants