Skip to content

Commit 17b9b8b

Browse files
committed
logout when restart the server
1 parent ea7e29b commit 17b9b8b

File tree

9 files changed

+267
-3
lines changed

9 files changed

+267
-3
lines changed

gateway-ha/src/main/java/io/trino/gateway/ha/config/FormAuthConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@
1313
*/
1414
package io.trino.gateway.ha.config;
1515

16+
import io.airlift.units.Duration;
17+
18+
import java.util.concurrent.TimeUnit;
19+
1620
public class FormAuthConfiguration
1721
{
1822
private SelfSignKeyPairConfiguration selfSignKeyPair;
1923
private String ldapConfigPath;
24+
private Duration sessionTimeout = new Duration(30, TimeUnit.MINUTES);
2025

2126
public FormAuthConfiguration(SelfSignKeyPairConfiguration selfSignKeyPair, String ldapConfigPath)
2227
{
@@ -45,4 +50,14 @@ public void setLdapConfigPath(String ldapConfigPath)
4550
{
4651
this.ldapConfigPath = ldapConfigPath;
4752
}
53+
54+
public Duration getSessionTimeout()
55+
{
56+
return this.sessionTimeout;
57+
}
58+
59+
public void setSessionTimeout(Duration sessionTimeout)
60+
{
61+
this.sessionTimeout = sessionTimeout;
62+
}
4863
}

gateway-ha/src/main/java/io/trino/gateway/ha/resource/LoginResource.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,18 @@ else if (oauthManager != null) {
185185
}
186186
return Response.ok(Result.ok("Ok", loginType)).build();
187187
}
188+
189+
@POST
190+
@Path("serverInfo")
191+
@Consumes(MediaType.APPLICATION_JSON)
192+
@Produces(MediaType.APPLICATION_JSON)
193+
public Response serverInfo()
194+
{
195+
long serverStartTime = System.currentTimeMillis();
196+
if (formAuthManager != null) {
197+
serverStartTime = formAuthManager.getServerStartTime();
198+
}
199+
Map<String, Object> serverInfo = Map.of("serverStart", serverStartTime);
200+
return Response.ok(Result.ok(serverInfo)).build();
201+
}
188202
}

gateway-ha/src/main/java/io/trino/gateway/ha/security/LbFormAuthManager.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,21 @@
1919
import com.auth0.jwt.interfaces.Claim;
2020
import com.auth0.jwt.interfaces.DecodedJWT;
2121
import io.airlift.log.Logger;
22+
import io.airlift.units.Duration;
2223
import io.trino.gateway.ha.config.FormAuthConfiguration;
2324
import io.trino.gateway.ha.config.LdapConfiguration;
2425
import io.trino.gateway.ha.config.UserConfiguration;
2526
import io.trino.gateway.ha.domain.Result;
2627
import io.trino.gateway.ha.domain.request.RestLoginRequest;
2728
import io.trino.gateway.ha.security.util.BasicCredentials;
2829

30+
import java.time.Instant;
2931
import java.util.Collections;
32+
import java.util.Date;
3033
import java.util.List;
3134
import java.util.Map;
3235
import java.util.Optional;
36+
import java.util.concurrent.TimeUnit;
3337
import java.util.stream.Stream;
3438

3539
import static com.google.common.collect.ImmutableMap.toImmutableMap;
@@ -38,13 +42,16 @@
3842
public class LbFormAuthManager
3943
{
4044
private static final Logger log = Logger.get(LbFormAuthManager.class);
45+
private static final long SERVER_START_TIME = System.currentTimeMillis();
46+
private static final Duration DEFAULT_SESSION_TIMEOUT = new Duration(30, TimeUnit.MINUTES);
4147
/**
4248
* Cookie key to pass the token.
4349
*/
4450
private final LbKeyProvider lbKeyProvider;
4551
private final Map<String, UserConfiguration> presetUsers;
4652
private final Map<String, String> pagePermissions;
4753
private final LbLdapClient lbLdapClient;
54+
private final Duration sessionTimeout;
4855

4956
public LbFormAuthManager(FormAuthConfiguration configuration,
5057
Map<String, UserConfiguration> presetUsers,
@@ -58,9 +65,12 @@ public LbFormAuthManager(FormAuthConfiguration configuration,
5865
if (configuration != null) {
5966
this.lbKeyProvider = new LbKeyProvider(configuration
6067
.getSelfSignKeyPair());
68+
this.sessionTimeout = configuration.getSessionTimeout() != null ?
69+
configuration.getSessionTimeout() : DEFAULT_SESSION_TIMEOUT;
6170
}
6271
else {
6372
this.lbKeyProvider = null;
73+
this.sessionTimeout = DEFAULT_SESSION_TIMEOUT;
6474
}
6575

6676
if (configuration != null && configuration.getLdapConfigPath() != null) {
@@ -105,6 +115,21 @@ public Optional<Map<String, Claim>> getClaimsFromIdToken(String idToken)
105115
DecodedJWT jwt = JWT.decode(idToken);
106116

107117
if (LbTokenUtil.validateToken(idToken, lbKeyProvider.getRsaPublicKey(), jwt.getIssuer(), Optional.empty())) {
118+
// Check if token was issued before server restart
119+
Optional<Claim> serverStartClaim = Optional.ofNullable(jwt.getClaim("server_start"));
120+
if (serverStartClaim.isPresent() && !serverStartClaim.orElseThrow().isNull()) {
121+
long tokenServerStart = serverStartClaim.orElseThrow().asLong();
122+
if (tokenServerStart != SERVER_START_TIME) {
123+
log.info("Token invalidated due to server restart");
124+
return Optional.empty();
125+
}
126+
}
127+
// Check token expiration
128+
Optional<Date> expiresAt = Optional.ofNullable(jwt.getExpiresAt());
129+
if (expiresAt.isPresent() && expiresAt.orElseThrow().before(new Date())) {
130+
log.info("Token expired");
131+
return Optional.empty();
132+
}
108133
return Optional.of(jwt.getClaims());
109134
}
110135
}
@@ -124,10 +149,16 @@ private String getSelfSignedToken(String username)
124149

125150
Map<String, Object> headers = Map.of("alg", "RS256");
126151

152+
Instant now = Instant.now();
153+
Instant expiration = now.plusSeconds(sessionTimeout.roundTo(TimeUnit.SECONDS));
154+
127155
token = JWT.create()
128156
.withHeader(headers)
129157
.withIssuer(SessionCookie.SELF_ISSUER_ID)
130158
.withSubject(username)
159+
.withIssuedAt(Date.from(now))
160+
.withExpiresAt(Date.from(expiration))
161+
.withClaim("server_start", SERVER_START_TIME)
131162
.sign(algorithm);
132163
}
133164
catch (JWTCreationException exception) {
@@ -167,4 +198,9 @@ public List<String> processPagePermissions(List<String> roles)
167198
.flatMap(role -> Stream.of(pagePermissions.get(role).split("_")))
168199
.distinct().toList();
169200
}
201+
202+
public long getServerStartTime()
203+
{
204+
return SERVER_START_TIME;
205+
}
170206
}

gateway-ha/src/main/java/io/trino/gateway/ha/security/SessionCookie.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static NewCookie getTokenCookie(String token)
3030
.path("/")
3131
.domain("")
3232
.comment("")
33-
.maxAge(60 * 60 * 24)
33+
.maxAge(60 * 15) // 15 minutes session timeout
3434
.secure(true)
3535
.build();
3636
}

webapp/src/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useEffect } from 'react';
1616
import { getCSSVar } from './utils/utils';
1717
import { IllustrationIdle, IllustrationIdleDark } from '@douyinfe/semi-illustrations';
1818
import Cookies from 'js-cookie';
19+
import { SessionManager } from './utils/session';
1920

2021
function App() {
2122
return (
@@ -40,6 +41,18 @@ function Screen() {
4041
access.updateToken(token);
4142
Cookies.remove('token');
4243
}
44+
// Initialize session management
45+
const sessionManager = SessionManager.getInstance();
46+
sessionManager.setSessionExpiredCallback(() => {
47+
access.logout();
48+
});
49+
50+
// Check token validity on app start
51+
access.checkTokenValidity().catch(console.error);
52+
53+
return () => {
54+
sessionManager.clearTimeout();
55+
};
4356
}, [])
4457
return (
4558
<>

webapp/src/api/base.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { useAccessStore } from "../store";
22
import Locale, { getServerLang } from "../locales";
33
import { Toast } from "@douyinfe/semi-ui";
4+
import { SessionManager } from "../utils/session";
45

56
export class ClientApi {
67
async get(url: string, params: Record<string, any> = {}): Promise<any> {
8+
// Check token validity before making request
9+
await this.validateTokenBeforeRequest(url);
710
let queryString = "";
811
if (Object.keys(params).length > 0) {
912
queryString = "?" + new URLSearchParams(params).toString();
@@ -40,6 +43,8 @@ export class ClientApi {
4043
}
4144

4245
async post(url: string, body: Record<string, any> = {}): Promise<any> {
46+
// Check token validity before making request
47+
await this.validateTokenBeforeRequest(url);
4348
const res: Response = await fetch(
4449
this.path(url),
4550
{
@@ -76,6 +81,8 @@ export class ClientApi {
7681
}
7782

7883
async postForm(url: string, formData: FormData = new FormData()): Promise<any> {
84+
// Check token validity before making request
85+
await this.validateTokenBeforeRequest(url);
7986
const res: Response = await fetch(
8087
this.path(url),
8188
{
@@ -104,6 +111,26 @@ export class ClientApi {
104111
return resJson.data;
105112
}
106113

114+
private async validateTokenBeforeRequest(url: string): Promise<void> {
115+
// Skip validation for login-related endpoints to avoid infinite loops
116+
if (url.includes('/login') || url.includes('/serverInfo') || url.includes('/loginType')) {
117+
return;
118+
}
119+
120+
const accessStore = useAccessStore.getState();
121+
if (accessStore.token) {
122+
try {
123+
const isValid = await accessStore.checkTokenValidity();
124+
if (!isValid) {
125+
throw new Error('Token validation failed');
126+
}
127+
} catch (error) {
128+
// Token validation failed, user will be logged out
129+
throw error;
130+
}
131+
}
132+
}
133+
107134
path(path: string): string {
108135
const proxyPath = import.meta.env.VITE_PROXY_PATH;
109136
return [proxyPath, path].join("");
@@ -134,7 +161,12 @@ export function getHeaders(): Record<string, string> {
134161
const validString = (x: string) => x && x.length > 0;
135162

136163
if (validString(accessStore.token)) {
137-
headers.Authorization = makeBearer(accessStore.token);
164+
// For synchronous header generation, we'll do basic token validation
165+
// The async server restart check will happen in the session manager
166+
const sessionManager = SessionManager.getInstance();
167+
if (!sessionManager.isTokenExpired(accessStore.token)) {
168+
headers.Authorization = makeBearer(accessStore.token);
169+
}
138170
}
139171

140172
return headers;

webapp/src/api/webapp/login.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ export async function loginTypeApi(): Promise<any> {
2323
export async function getUIConfiguration(): Promise<any> {
2424
return api.get('/webapp/getUIConfiguration')
2525
}
26+
27+
export async function serverInfoApi(): Promise<any> {
28+
return api.post('/serverInfo', {})
29+
}

webapp/src/store/access.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { create } from "zustand";
22
import { persist } from "zustand/middleware";
33
import { StoreKey } from "../constant";
4-
import { getInfoApi } from "../api/webapp/login";
4+
import { getInfoApi, serverInfoApi } from "../api/webapp/login";
5+
import { SessionManager } from "../utils/session";
56

67
export enum Role {
78
ADMIN = "ADMIN",
@@ -28,6 +29,8 @@ export interface AccessControlStore {
2829
getUserInfo: (_?: boolean) => void;
2930
hasRole: (role: Role) => boolean;
3031
hasPermission: (permission: string | undefined) => boolean;
32+
logout: () => void;
33+
checkTokenValidity: () => Promise<boolean>;
3134
}
3235

3336
let fetchState: number = 0; // 0 not fetch, 1 fetching, 2 done
@@ -78,6 +81,51 @@ export const useAccessStore = create<AccessControlStore>()(
7881
const permissions = get().permissions
7982
return permission == undefined || permissions == null || permissions.length == 0 || permissions.includes(permission);
8083
},
84+
logout() {
85+
const sessionManager = SessionManager.getInstance();
86+
sessionManager.clearTimeout();
87+
set(() => ({
88+
token: "",
89+
userId: "",
90+
userName: "",
91+
nickName: "",
92+
userType: "",
93+
email: "",
94+
phonenumber: "",
95+
sex: "",
96+
avatar: "",
97+
permissions: [],
98+
roles: [],
99+
}));
100+
fetchState = 0;
101+
},
102+
async checkTokenValidity() {
103+
const token = get().token;
104+
if (!token) return false;
105+
106+
const sessionManager = SessionManager.getInstance();
107+
108+
// Check if token is expired
109+
if (sessionManager.isTokenExpired(token)) {
110+
get().logout();
111+
return false;
112+
}
113+
114+
// Check for server restart
115+
try {
116+
const serverInfo = await serverInfoApi();
117+
if (sessionManager.checkServerRestart(token, serverInfo.serverStart)) {
118+
console.log('Server restart detected, logging out');
119+
get().logout();
120+
return false;
121+
}
122+
} catch (error) {
123+
console.error('Error checking server info:', error);
124+
// Don't logout on API error, just continue
125+
}
126+
127+
return true;
128+
},
81129
}),
82130
{
83131
name: StoreKey.Access,

0 commit comments

Comments
 (0)