Skip to content

Commit dff2119

Browse files
authored
Merge pull request #589 from lowcoder-org/update-refresh-token-handling
Rework Oauth Refresh Token Handling
2 parents 722af3c + 2b8addc commit dff2119

File tree

7 files changed

+98
-41
lines changed

7 files changed

+98
-41
lines changed

client/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.2.1
1+
dev

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public class User extends HasIdAndAuditing implements BeforeMongodbWrite, AfterM
4646

4747
private Boolean isEnabled = true;
4848

49+
private String activeAuthId;
50+
4951
// used in form login
5052
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
5153
private String password;

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,10 @@ public Mono<Boolean> addNewConnection(String userId, Connection connection) {
210210
@Override
211211
public Mono<User> addNewConnectionAndReturnUser(String userId, Connection connection) {
212212
return findById(userId)
213-
.doOnNext(user -> user.getConnections().add(connection))
213+
.doOnNext(user -> {
214+
user.getConnections().add(connection);
215+
user.setActiveAuthId(connection.getAuthId());
216+
})
214217
.flatMap(repository::save);
215218
}
216219

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/KeycloakRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ protected Mono<AuthToken> refreshAuthToken(String refreshToken) {
9494
.accessToken(MapUtils.getString(map, "access_token"))
9595
.expireIn(MapUtils.getIntValue(map, "expires_in"))
9696
.refreshToken(MapUtils.getString(map, "refresh_token"))
97+
.refreshTokenExpireIn(MapUtils.getIntValue(map, "refresh_expires_in"))
9798
.build();
9899
return Mono.just(authToken);
99100
});

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ public void updateConnection(AuthUser authUser, User user) {
242242
oldConnection.setAuthConnectionAuthToken(
243243
Optional.ofNullable(authUser.getAuthToken()).map(ConnectionAuthToken::of).orElse(null));
244244
oldConnection.setRawUserInfo(authUser.getRawUserInfo());
245+
246+
user.setActiveAuthId(oldConnection.getAuthId());
245247
}
246248

247249
@SuppressWarnings("OptionalGetWithoutIsPresent")
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package org.lowcoder.api.framework.filter;
22

33
import lombok.extern.slf4j.Slf4j;
4+
import org.apache.commons.lang3.tuple.Triple;
45
import org.lowcoder.api.authentication.request.AuthRequest;
56
import org.lowcoder.api.authentication.request.AuthRequestFactory;
67
import org.lowcoder.api.authentication.request.oauth2.OAuth2RequestContext;
78
import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl;
89
import org.lowcoder.api.home.SessionUserService;
910
import org.lowcoder.domain.authentication.AuthenticationService;
10-
import org.lowcoder.domain.authentication.FindAuthConfig;
1111
import org.lowcoder.domain.authentication.context.AuthRequestContext;
1212
import org.lowcoder.domain.user.model.AuthUser;
13+
import org.lowcoder.domain.user.model.Connection;
14+
import org.lowcoder.domain.user.model.User;
15+
import org.lowcoder.domain.user.service.UserService;
1316
import org.lowcoder.sdk.util.CookieHelper;
1417
import org.springframework.web.server.ServerWebExchange;
1518
import org.springframework.web.server.WebFilter;
@@ -18,8 +21,7 @@
1821

1922
import javax.annotation.Nonnull;
2023
import java.time.Instant;
21-
import java.util.LinkedList;
22-
import java.util.List;
24+
import java.util.Optional;
2325

2426
import static org.lowcoder.api.authentication.util.AuthenticationUtils.toAuthentication;
2527
import static org.lowcoder.domain.authentication.AuthenticationService.DEFAULT_AUTH_CONFIG;
@@ -29,6 +31,8 @@
2931
public class UserSessionPersistenceFilter implements WebFilter {
3032

3133
private final SessionUserService service;
34+
35+
private final UserService userService;
3236
private final CookieHelper cookieHelper;
3337

3438
private final AuthenticationService authenticationService;
@@ -37,9 +41,10 @@ public class UserSessionPersistenceFilter implements WebFilter {
3741

3842
private final AuthRequestFactory<AuthRequestContext> authRequestFactory;
3943

40-
public UserSessionPersistenceFilter(SessionUserService service, CookieHelper cookieHelper, AuthenticationService authenticationService,
44+
public UserSessionPersistenceFilter(SessionUserService service, UserService userService, CookieHelper cookieHelper, AuthenticationService authenticationService,
4145
AuthenticationApiServiceImpl authenticationApiService, AuthRequestFactory<AuthRequestContext> authRequestFactory) {
4246
this.service = service;
47+
this.userService = userService;
4348
this.cookieHelper = cookieHelper;
4449
this.authenticationService = authenticationService;
4550
this.authenticationApiService = authenticationApiService;
@@ -52,48 +57,88 @@ public Mono<Void> filter(@Nonnull ServerWebExchange exchange, WebFilterChain cha
5257
String cookieToken = cookieHelper.getCookieToken(exchange);
5358
return service.resolveSessionUserFromCookie(cookieToken)
5459
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
55-
.doOnNext(user -> {
60+
.map(user -> {
5661

57-
List<String> tokensToRemove = new LinkedList<>();
62+
Connection activeConnection = null;
63+
String orgId = null;
5864

59-
user.getConnections().forEach(connection -> {
60-
if(!connection.getAuthId().equals(DEFAULT_AUTH_CONFIG.getId())) {
61-
Instant next5Minutes = Instant.now().plusSeconds( 300 );
62-
if(connection.getAuthConnectionAuthToken().getExpireAt() == 0) {
63-
return;
64-
}
65-
boolean isAccessTokenExpiryNear = (connection.getAuthConnectionAuthToken().getExpireAt()*1000) <= next5Minutes.toEpochMilli();
66-
if(isAccessTokenExpiryNear) {
67-
connection.getOrgIds().forEach(orgId -> {
68-
authenticationService.findAuthConfigByAuthId(orgId, connection.getAuthId())
69-
.doOnSuccess(findAuthConfig -> {
70-
if(findAuthConfig == null) {
71-
return;
72-
}
73-
OAuth2RequestContext oAuth2RequestContext = new OAuth2RequestContext(orgId, null, null);
74-
oAuth2RequestContext.setAuthConfig(findAuthConfig.authConfig());
75-
AuthRequest authRequest = authRequestFactory.build(oAuth2RequestContext).block();
76-
try {
77-
AuthUser authUser = authRequest.refresh(connection.getAuthConnectionAuthToken().getRefreshToken()).block();
78-
authUser.setAuthContext(oAuth2RequestContext);
79-
authenticationApiService.updateConnection(authUser, user);
80-
} catch (Exception e) {
81-
log.error("Failed to refresh access token. Removing user sessions/tokens.");
82-
tokensToRemove.addAll(connection.getTokens());
83-
}
84-
});
85-
});
65+
Optional<Connection> activeConnectionOptional = user.getConnections()
66+
.stream()
67+
.filter(connection -> connection.getAuthId().equals(user.getActiveAuthId()))
68+
.findFirst();
69+
70+
if(!activeConnectionOptional.isPresent()) {
71+
return Triple.of(user, activeConnection, orgId);
72+
}
73+
74+
activeConnection = activeConnectionOptional.get();
75+
76+
if(!activeConnection.getAuthId().equals(DEFAULT_AUTH_CONFIG.getId())) {
77+
if(activeConnection.getAuthConnectionAuthToken().getExpireAt() == 0) {
78+
return Triple.of(user, activeConnection, orgId);
79+
}
80+
boolean isAccessTokenExpired = (activeConnection.getAuthConnectionAuthToken().getExpireAt()*1000) < Instant.now().toEpochMilli();
81+
if(isAccessTokenExpired) {
82+
83+
Optional<String> orgIdOptional = activeConnection.getOrgIds().stream().findFirst();
84+
if(!orgIdOptional.isPresent()) {
85+
return Triple.of(user, activeConnection, orgId);
8686
}
87+
orgId = orgIdOptional.get();
8788
}
88-
});
89+
}
8990

90-
tokensToRemove.forEach(token -> {
91-
service.removeUserSession(token).block();
92-
});
91+
return Triple.of(user, activeConnection, orgId);
9392

94-
})
93+
}).flatMap(this::refreshOauthToken)
9594
.flatMap(user -> chain.filter(exchange).contextWrite(withAuthentication(toAuthentication(user)))
9695
.then(service.extendValidity(cookieToken))
9796
);
9897
}
98+
99+
private Mono<User> refreshOauthToken(Triple<User, Connection, String> triple) {
100+
101+
User user = triple.getLeft();
102+
Connection connection = triple.getMiddle();
103+
String orgId = triple.getRight();
104+
105+
if (connection == null || orgId == null) {
106+
return Mono.just(user);
107+
}
108+
109+
OAuth2RequestContext oAuth2RequestContext = new OAuth2RequestContext(triple.getRight(), null, null);
110+
111+
return authenticationService
112+
.findAuthConfigByAuthId(orgId, connection.getAuthId())
113+
.switchIfEmpty(Mono.empty())
114+
.flatMap(findAuthConfig -> {
115+
116+
Mono<AuthRequest> authRequestMono = Mono.empty();
117+
118+
if(findAuthConfig == null) {
119+
return authRequestMono;
120+
}
121+
oAuth2RequestContext.setAuthConfig(findAuthConfig.authConfig());
122+
123+
return authRequestFactory.build(oAuth2RequestContext);
124+
}).flatMap(authRequest -> {
125+
if(authRequest == null) {
126+
return Mono.just(user);
127+
}
128+
try {
129+
AuthUser authUser = authRequest.refresh(connection.getAuthConnectionAuthToken().getRefreshToken()).block();
130+
authUser.setAuthContext(oAuth2RequestContext);
131+
authenticationApiService.updateConnection(authUser, user);
132+
return userService.update(user.getId(), user);
133+
} catch (Exception e) {
134+
log.error("Failed to refresh access token. Removing user sessions/tokens.");
135+
connection.getTokens().forEach(token -> {
136+
service.removeUserSession(token).block();
137+
});
138+
}
139+
return Mono.just(user);
140+
});
141+
142+
}
143+
99144
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.lowcoder.domain.authentication.AuthenticationService;
1111
import org.lowcoder.domain.authentication.context.AuthRequestContext;
1212
import org.lowcoder.domain.user.model.User;
13+
import org.lowcoder.domain.user.service.UserService;
1314
import org.lowcoder.infra.constant.NewUrl;
1415
import org.lowcoder.sdk.config.CommonConfig;
1516
import org.lowcoder.sdk.util.CookieHelper;
@@ -50,6 +51,9 @@ public class SecurityConfig {
5051
@Autowired
5152
private SessionUserService sessionUserService;
5253

54+
@Autowired
55+
private UserService userService;
56+
5357
@Autowired
5458
private AccessDeniedHandler accessDeniedHandler;
5559

@@ -153,7 +157,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
153157
.accessDeniedHandler(accessDeniedHandler)
154158
);
155159

156-
http.addFilterBefore(new UserSessionPersistenceFilter(sessionUserService, cookieHelper, authenticationService, authenticationApiService, authRequestFactory), SecurityWebFiltersOrder.AUTHENTICATION);
160+
http.addFilterBefore(new UserSessionPersistenceFilter(sessionUserService, userService, cookieHelper, authenticationService, authenticationApiService, authRequestFactory), SecurityWebFiltersOrder.AUTHENTICATION);
157161
http.addFilterBefore(new APIKeyAuthFilter(sessionUserService, cookieHelper, jwtUtils), SecurityWebFiltersOrder.AUTHENTICATION);
158162

159163
return http.build();

0 commit comments

Comments
 (0)