Skip to content

Conversation

@MichalJura
Copy link
Contributor

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

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.

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

}

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

Development

Successfully merging this pull request may close these issues.

Don't force relogin on expired token, when anonymous user has enough permissions. The default keycloak/openid logintimeout is very short

3 participants