Skip to content

Commit

Permalink
fix(ui): prevent login loop when token expires without a valid refres…
Browse files Browse the repository at this point in the history
…h token (#395)

* fix(ui): prevent login loop when token expires without a valid refresh token

* update submodule

* ent oidc tests - WIP

* ent oidc tests - WIP

* ent oidc tests - WIP

* ent oidc tests - WIP

* ent oidc tests

* update submodule
  • Loading branch information
glasstiger authored Feb 13, 2025
1 parent e30e64a commit 901e9e3
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/browser-tests/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = defineConfig({
screenshotOnRunFailure: false,
video: false,
baseUrl: baseUrl,
chromeWebSecurity: false, //if it is true, cypress does not allow redirects
viewportWidth: 1280,
viewportHeight: 720,
specPattern: "cypress/integration/**/*.spec.js",
Expand Down
18 changes: 18 additions & 0 deletions packages/browser-tests/cypress/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,24 @@ Cypress.Commands.add("loadConsoleWithAuth", (clearWarnings) => {
}
});

Cypress.Commands.add("loadConsoleAsAdminAndCreateSSOGroup", (group, externalGroup = undefined) => {
cy.loadConsoleWithAuth(true);
cy.executeSQL(`CREATE GROUP ${group} WITH EXTERNAL ALIAS ${externalGroup || group};`);
cy.executeSQL(`GRANT HTTP TO ${group};`);
cy.logout();
});

Cypress.Commands.add("logout", () => {
cy.getByDataHook("button-logout").click();
cy.getByDataHook("auth-login").should("be.visible");
});

Cypress.Commands.add("executeSQL", (sql) => {
cy.clearEditor();
cy.typeQuery(sql);
cy.clickRun();
});

Cypress.Commands.add("refreshSchema", () => {
// toggle between auto-refresh modes to trigger a schema refresh
cy.getByDataHook("schema-auto-refresh-button").click();
Expand Down
99 changes: 99 additions & 0 deletions packages/browser-tests/cypress/integration/enterprise/oidc.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/// <reference types="cypress" />

const contextPath = process.env.QDB_HTTP_CONTEXT_WEB_CONSOLE || ""
const baseUrl = `http://localhost:9999${contextPath}`;
const settingsUrl = `${baseUrl}/settings`;

const oidcProviderUrl = "http://localhost:9032";
const oidcAuthorizationCodeUrl = `${oidcProviderUrl}/authorization`;
const oidcTokenUrl = `${oidcProviderUrl}/token`;

const interceptSettings = (payload) => {
cy.intercept({ method: "GET", url: settingsUrl }, payload).as(
"settings"
);
};

const interceptAuthorizationCodeRequest = (redirectUrl) => {
cy.intercept("GET", `${oidcAuthorizationCodeUrl}?**`, (req) => {
req.redirect(redirectUrl);
}).as('authorizationCode');
};

const interceptTokenRequest = (payload) => {
cy.intercept({ method: "POST", url: oidcTokenUrl }, payload).as(
"tokens"
);
};

describe("OIDC authentication", () => {
before(() => {
// setup SSO group mappings
cy.loadConsoleAsAdminAndCreateSSOGroup("group1");
});

beforeEach(() => {
// load login page
interceptSettings({
"release.type": "EE",
"release.version": "1.2.3",
"acl.enabled": true,
"acl.basic.auth.realm.enabled": false,
"acl.oidc.enabled": true,
"acl.oidc.client.id": "client1",
"acl.oidc.authorization.endpoint": oidcAuthorizationCodeUrl,
"acl.oidc.token.endpoint": oidcTokenUrl,
"acl.oidc.pkce.required": true,
"acl.oidc.state.required": false,
"acl.oidc.groups.encoded.in.token": false,
});
cy.visit(baseUrl);

cy.wait("@settings");
cy.getByDataHook("auth-login").should("be.visible");
cy.getByDataHook("button-sso-login").should("be.visible");
cy.getEditor().should("not.exist");
});

it("should login via OIDC", () => {
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`);
cy.getByDataHook("button-sso-login").click();
cy.wait("@authorizationCode");

interceptTokenRequest({
"access_token": "gslpJtzmmi6RwaPSx0dYGD4tEkom",
"refresh_token": "FUuAAqMp6LSTKmkUd5uZuodhiE4Kr6M7Eyv",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6I",
"token_type": "Bearer",
"expires_in": 300
});
cy.wait("@tokens");
cy.getEditor().should("be.visible");

cy.executeSQL("select current_user();");
cy.getGridRow(0).should("contain", "user1");

cy.logout();
});

it("should force authentication if token expired, and there is no refresh token", () => {
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`);
cy.getByDataHook("button-sso-login").click();
cy.wait("@authorizationCode");

interceptTokenRequest({
"access_token": "gslpJtzmmi6RwaPSx0dYGD4tEkom",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6I",
"token_type": "Bearer",
"expires_in": 0
});
cy.wait("@tokens");
cy.getEditor().should("be.visible");

cy.reload();
cy.getByDataHook("button-log-in").should("be.visible");

cy.getByDataHook("button-log-in").click()
cy.getEditor().should("be.visible");
});
});
1 change: 1 addition & 0 deletions packages/browser-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"test": "cypress run --env failOnSnapshotDiff=false --env requireSnapshots=false --spec 'cypress/integration/console/*.spec.js'",
"test:auth": "cypress run --env failOnSnapshotDiff=false --env requireSnapshots=false --spec 'cypress/integration/auth/*.spec.js'",
"test:enterprise": "cypress run --env failOnSnapshotDiff=false --env requireSnapshots=false --spec 'cypress/integration/enterprise/*.spec.js'",
"test:update": "yarn run test --env updateSnapshots=true"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-tests/questdb
Submodule questdb updated 34 files
+106 −0 .github/workflows/glibc_smoke_test.yml
+17 −9 .github/workflows/rebuild_native_libs.yml
+14 −5 .github/workflows/rebuild_rust.yml
+1 −1 core/pom.xml
+ core/src/main/bin/linux-aarch64/libjemalloc.so
+1 −10 core/src/main/c/toolchains/linux-arm64.cmake
+0 −5 core/src/main/java/io/questdb/cairo/sql/PageFrameMemoryRecord.java
+2 −1 core/src/main/java/io/questdb/cairo/sql/Record.java
+4 −6 core/src/main/java/io/questdb/cairo/sql/VirtualRecord.java
+54 −23 core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java
+0 −4 core/src/main/java/io/questdb/griffin/engine/functions/window/BaseDoubleWindowFunction.java
+0 −4 core/src/main/java/io/questdb/griffin/engine/functions/window/BaseLongWindowFunction.java
+82 −0 core/src/main/java/io/questdb/griffin/engine/functions/window/DenseRankFunctionFactory.java
+14 −3 core/src/main/java/io/questdb/griffin/engine/functions/window/LagDoubleFunctionFactory.java
+2 −2 core/src/main/java/io/questdb/griffin/engine/functions/window/LeadDoubleFunctionFactory.java
+310 −162 core/src/main/java/io/questdb/griffin/engine/functions/window/RankFunctionFactory.java
+6 −8 core/src/main/java/io/questdb/griffin/engine/functions/window/RowNumberFunctionFactory.java
+0 −5 core/src/main/java/io/questdb/griffin/engine/groupby/FillRangeRecordCursorFactory.java
+1 −4 core/src/main/java/io/questdb/griffin/engine/table/ShowCreateTableRecordCursorFactory.java
+12 −2 core/src/main/java/io/questdb/griffin/engine/window/WindowFunction.java
+12 −2 core/src/main/java/io/questdb/griffin/model/WindowColumn.java
+1 −0 core/src/main/java/module-info.java
+1 −0 core/src/main/resources/META-INF/services/io.questdb.griffin.FunctionFactory
+ core/src/main/resources/io/questdb/bin/linux-aarch64/libquestdb.so
+ core/src/main/resources/io/questdb/bin/linux-aarch64/libquestdbr.so
+ core/src/main/resources/io/questdb/bin/windows-x86-64/libquestdb.dll
+ core/src/main/resources/io/questdb/bin/windows-x86-64/questdbr.dll
+2 −1 core/src/test/java/io/questdb/test/TelemetryTest.java
+3 −1 core/src/test/java/io/questdb/test/cutlass/line/tcp/AbstractLineTcpReceiverFuzzTest.java
+1 −0 core/src/test/java/io/questdb/test/cutlass/line/tcp/LineTcpReceiverUpdateFuzzTest.java
+49 −0 core/src/test/java/io/questdb/test/griffin/OrderByWithFilterTest.java
+36 −2 core/src/test/java/io/questdb/test/griffin/ShowCreateTableTest.java
+33 −4 core/src/test/java/io/questdb/test/griffin/SqlOptimiserTest.java
+436 −155 core/src/test/java/io/questdb/test/griffin/engine/window/WindowFunctionTest.java
10 changes: 10 additions & 0 deletions packages/web-console/serve-dist.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ const server = http.createServer((req, res) => {
})

req.pipe(proxyReq, { end: true })
} else if (
reqPathName.startsWith("/userinfo")
) {
res.writeHead(200, { 'Content-Type': 'application/json' })
// TODO: should be able to set the response from the test
// add something like /setUserInfo?info={sub: "user2", groups: "bla"}
res.end(JSON.stringify({
sub: "user1",
groups: ["group1", "group2"]
}))
} else {
// serve static files from /dist folder
const filePath = path.join(
Expand Down
1 change: 1 addition & 0 deletions packages/web-console/src/components/TopBar/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const Toolbar = () => {
onClick={() => logout()}
prefixIcon={<LogoutCircle size="18px" />}
skin="secondary"
data-hook="button-logout"
>
Log out
</Button>
Expand Down
1 change: 1 addition & 0 deletions packages/web-console/src/modules/OAuth2/views/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const Error = ({
<Text color="foreground">{errorMessage}</Text>
{!basicAuthEnabled && (
<Button
data-hook="button-login-with-other-account"
skin="secondary"
prefixIcon={<User size="18px" />}
onClick={() => onLogout()}
Expand Down
5 changes: 4 additions & 1 deletion packages/web-console/src/modules/OAuth2/views/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,10 @@ export const Login = ({
validationSchema={schema}
>
<Form.Item name="username" label="User name">
<Form.Input name="username" placeholder={"johndoe"} />
<Form.Input
name="username"
placeholder={"johndoe"}
/>
</Form.Item>
<Form.Item name="password" label="Password">
<Form.Input
Expand Down
1 change: 1 addition & 0 deletions packages/web-console/src/modules/OAuth2/views/logout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const Logout = ({ onLogout }: { onLogout: () => void }) => (
<Box gap="1rem">
<Text color="foreground">You have been logged out.</Text>
<Button
data-hook="button-log-in"
prefixIcon={<LoginCircle size={18} />}
skin="secondary"
onClick={onLogout}
Expand Down
4 changes: 2 additions & 2 deletions packages/web-console/src/providers/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
await refreshAuthToken(settings)
} else {
// if there is no refresh token, user has to re-authenticate to get fresh tokens
dispatch({ view: View.loggedOut })
logout(true)
}
} else {
setSessionData(tokenResponse)
Expand All @@ -193,7 +193,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
removeValue(StoreKey.OAUTH_STATE)
const stateParam = urlParams.get("state")
if (!stateParam || state !== stateParam) {
dispatch({ view: View.loggedOut })
logout(true)
return
}
}
Expand Down
4 changes: 2 additions & 2 deletions run_browser_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ yarn workspace browser-tests test:auth

read -p "Press any key to continue... " -n1 -s

# Switch authentication on
# Switch OSS authentication on
export QDB_HTTP_USER=admin
export QDB_HTTP_PASSWORD=quest

# Running tests which assume authentication is on
# Running tests which assume that OSS authentication is on
./tmp/questdb-*/bin/questdb.sh start -d tmp/dbroot
yarn workspace browser-tests test
./tmp/questdb-*/bin/questdb.sh stop
Expand Down
53 changes: 53 additions & 0 deletions run_ent_browser_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash -x

# Run it from the 'ui' directory as:
# ./run_ent_browser_tests.sh

# Cleanup
rm -rf packages/browser-tests/cypress/snapshots/*
rm -rf tmp/dbroot
rm -rf tmp/questdb-*

# Clone questdb-enterprise
git clone https://github.com/questdb/questdb-enterprise.git tmp/questdb-enterprise
cd tmp/questdb-enterprise || exit 1
git submodule init
git submodule update
cd ../..

# Build server
mvn clean package -e -f tmp/questdb-enterprise/pom.xml -DskipTests -P build-ent-binaries 2>&1

# Unpack server
tar xzf tmp/questdb-enterprise/questdb-ent/target/questdb-enterprise-*-rt-*.tar.gz -C tmp/
mkdir tmp/dbroot

# Build web console
yarn install --immutable --immutable-cache
yarn workspace @questdb/react-components run build
yarn workspace @questdb/web-console run build

# Start proxy
node packages/web-console/serve-dist.js &
PID1="$!"
echo "Proxy started, PID=$PID1"

# Switch dev mode on
export QDB_DEV_MODE_ENABLED=true

# OIDC config
export QDB_ACL_OIDC_ENABLED=true
export QDB_ACL_OIDC_TLS_ENABLED=false
export QDB_ACL_OIDC_GROUPS_CLAIM=groups
export QDB_ACL_OIDC_CLIENT_ID=clientId
export QDB_ACL_OIDC_HOST=localhost
export QDB_ACL_OIDC_PORT=9999
export QDB_ACL_OIDC_USERINFO_ENDPOINT=/userinfo

# Running tests which assume authentication is off
./tmp/questdb-*/bin/questdb.sh start -d tmp/dbroot
yarn workspace browser-tests test:enterprise
./tmp/questdb-*/bin/questdb.sh stop

# Stop proxy
kill -SIGTERM $PID1

0 comments on commit 901e9e3

Please sign in to comment.