Skip to content

Rework Oauth Refresh Token Handling #589

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

Merged
merged 52 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
70a2284
Link accounts based on common authid(email)
aq-ikhwa-tech Dec 5, 2023
e2bc358
Merge branch 'dev' into fix-oauth-issues
aq-ikhwa-tech Dec 5, 2023
afa3104
Finalize fixes
aq-ikhwa-tech Dec 6, 2023
5cc8acd
Merge branch 'dev' into fix-oauth-issues
aq-ikhwa-tech Dec 6, 2023
0bc105c
fixing plugin creator
Dec 6, 2023
5772a7a
fixing plugin creator 2
Dec 6, 2023
6d02703
fix plugin creator
Dec 6, 2023
2d345e8
fix plugin creator 3
Dec 6, 2023
f615728
fix documentation for plugin creator
Dec 6, 2023
cf6b9fe
plugin creator fix dependencies
Dec 6, 2023
7440e1a
plugin creator fix dependencies
Dec 6, 2023
756b294
Merge pull request #567 from lowcoder-org/fix-plugin-creator
FalkWolsky Dec 6, 2023
669b379
plugin creator fix dependencies
Dec 6, 2023
885d0dc
Bump Vite to 4.5.1
Dec 6, 2023
efa905a
Updated Component Plugin Demo
Dec 6, 2023
02327cf
Updated Component Plugin Demo
Dec 6, 2023
53afb73
Merge pull request #566 from lowcoder-org/fix-oauth-issues
FalkWolsky Dec 7, 2023
2f4504f
fix: fix build issues
raheeliftikhar5 Dec 7, 2023
1873519
small fix
raheeliftikhar5 Dec 7, 2023
d516538
lock file
raheeliftikhar5 Dec 7, 2023
9979f42
Merge pull request #568 from raheeliftikhar5/build-fix
FalkWolsky Dec 7, 2023
98ab214
Fixes in workspaces anc comp includes
Dec 7, 2023
9595937
Fixes in workspaces anc comp includes
Dec 7, 2023
71148c1
fix: avoid editor popup close on selecting suggeston using mouse
raheeliftikhar5 Dec 7, 2023
f606a4c
fix: enable scroll in table columns configs popup
raheeliftikhar5 Dec 7, 2023
b1e50c4
fix: avoid editor popup close when open from config popup
raheeliftikhar5 Dec 8, 2023
89d21d0
fix: date input validation fix
raheeliftikhar5 Dec 8, 2023
a41adbe
fix: table status column's text color fix
raheeliftikhar5 Dec 8, 2023
fab5301
Reverted changes for Workspaces.
Dec 8, 2023
8d7b3d1
Merge pull request #569 from raheeliftikhar5/editor-fixes
FalkWolsky Dec 8, 2023
c0896af
Merge pull request #570 from raheeliftikhar5/issue-369/date-input-val…
FalkWolsky Dec 8, 2023
cb5bff0
Merge pull request #571 from raheeliftikhar5/table-status-column-style
FalkWolsky Dec 8, 2023
2b20d3f
Reverted changes for Workspaces.
Dec 8, 2023
6845453
Add functionality to allow users to link to auth providers while bein…
aq-ikhwa-tech Dec 9, 2023
95aae3c
Add handling for LOWCODER_CREATE_SIGNUP_WORKSPACE
aq-ikhwa-tech Dec 9, 2023
0a96750
Merge branch 'dev' into link-oauth-providers-for-existing-users
FalkWolsky Dec 9, 2023
b7c4928
new: simplify api service build
ludomikula Dec 6, 2023
b0d08a8
new: allow serving static files from mounted volume
ludomikula Dec 10, 2023
b106575
Merge pull request #572 from lowcoder-org/link-oauth-providers-for-ex…
FalkWolsky Dec 10, 2023
6292b35
Merge branch 'dev' into docker_build_update
FalkWolsky Dec 10, 2023
4304708
Merge pull request #574 from lowcoder-org/docker_build_update
FalkWolsky Dec 11, 2023
757d772
removed lowcoder-dev-utils
raheeliftikhar5 Dec 13, 2023
8130928
fix build issues
raheeliftikhar5 Dec 13, 2023
e9a75c0
Merge pull request #578 from raheeliftikhar5/remove-dev-utils
FalkWolsky Dec 13, 2023
3a74b79
remove create-lowcoder-plugin + version upgrade for publish
raheeliftikhar5 Dec 13, 2023
6170b11
Merge pull request #579 from raheeliftikhar5/publish-modules
FalkWolsky Dec 13, 2023
990ff84
Custom plugin publishing issues (#583)
raheeliftikhar5 Dec 14, 2023
ed99678
added hover and active color options in link styles (#585)
raheeliftikhar5 Dec 15, 2023
704e079
Merge branch 'main' into dev
ludomikula Dec 18, 2023
9b319cd
Try to rework oauth refresh token handling in reactive manner
aq-ikhwa-tech Dec 18, 2023
c95d040
Merge branch 'dev' into update-refresh-token-handling
aq-ikhwa-tech Dec 18, 2023
2b8addc
Merge branch 'dev' into update-refresh-token-handling
FalkWolsky Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.2.1
dev
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public class User extends HasIdAndAuditing implements BeforeMongodbWrite, AfterM

private Boolean isEnabled = true;

private String activeAuthId;

// used in form login
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,10 @@ public Mono<Boolean> addNewConnection(String userId, Connection connection) {
@Override
public Mono<User> addNewConnectionAndReturnUser(String userId, Connection connection) {
return findById(userId)
.doOnNext(user -> user.getConnections().add(connection))
.doOnNext(user -> {
user.getConnections().add(connection);
user.setActiveAuthId(connection.getAuthId());
})
.flatMap(repository::save);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ protected Mono<AuthToken> refreshAuthToken(String refreshToken) {
.accessToken(MapUtils.getString(map, "access_token"))
.expireIn(MapUtils.getIntValue(map, "expires_in"))
.refreshToken(MapUtils.getString(map, "refresh_token"))
.refreshTokenExpireIn(MapUtils.getIntValue(map, "refresh_expires_in"))
.build();
return Mono.just(authToken);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ public void updateConnection(AuthUser authUser, User user) {
oldConnection.setAuthConnectionAuthToken(
Optional.ofNullable(authUser.getAuthToken()).map(ConnectionAuthToken::of).orElse(null));
oldConnection.setRawUserInfo(authUser.getRawUserInfo());

user.setActiveAuthId(oldConnection.getAuthId());
}

@SuppressWarnings("OptionalGetWithoutIsPresent")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package org.lowcoder.api.framework.filter;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Triple;
import org.lowcoder.api.authentication.request.AuthRequest;
import org.lowcoder.api.authentication.request.AuthRequestFactory;
import org.lowcoder.api.authentication.request.oauth2.OAuth2RequestContext;
import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl;
import org.lowcoder.api.home.SessionUserService;
import org.lowcoder.domain.authentication.AuthenticationService;
import org.lowcoder.domain.authentication.FindAuthConfig;
import org.lowcoder.domain.authentication.context.AuthRequestContext;
import org.lowcoder.domain.user.model.AuthUser;
import org.lowcoder.domain.user.model.Connection;
import org.lowcoder.domain.user.model.User;
import org.lowcoder.domain.user.service.UserService;
import org.lowcoder.sdk.util.CookieHelper;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
Expand All @@ -18,8 +21,7 @@

import javax.annotation.Nonnull;
import java.time.Instant;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;

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

private final SessionUserService service;

private final UserService userService;
private final CookieHelper cookieHelper;

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

private final AuthRequestFactory<AuthRequestContext> authRequestFactory;

public UserSessionPersistenceFilter(SessionUserService service, CookieHelper cookieHelper, AuthenticationService authenticationService,
public UserSessionPersistenceFilter(SessionUserService service, UserService userService, CookieHelper cookieHelper, AuthenticationService authenticationService,
AuthenticationApiServiceImpl authenticationApiService, AuthRequestFactory<AuthRequestContext> authRequestFactory) {
this.service = service;
this.userService = userService;
this.cookieHelper = cookieHelper;
this.authenticationService = authenticationService;
this.authenticationApiService = authenticationApiService;
Expand All @@ -52,48 +57,88 @@ public Mono<Void> filter(@Nonnull ServerWebExchange exchange, WebFilterChain cha
String cookieToken = cookieHelper.getCookieToken(exchange);
return service.resolveSessionUserFromCookie(cookieToken)
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.doOnNext(user -> {
.map(user -> {

List<String> tokensToRemove = new LinkedList<>();
Connection activeConnection = null;
String orgId = null;

user.getConnections().forEach(connection -> {
if(!connection.getAuthId().equals(DEFAULT_AUTH_CONFIG.getId())) {
Instant next5Minutes = Instant.now().plusSeconds( 300 );
if(connection.getAuthConnectionAuthToken().getExpireAt() == 0) {
return;
}
boolean isAccessTokenExpiryNear = (connection.getAuthConnectionAuthToken().getExpireAt()*1000) <= next5Minutes.toEpochMilli();
if(isAccessTokenExpiryNear) {
connection.getOrgIds().forEach(orgId -> {
authenticationService.findAuthConfigByAuthId(orgId, connection.getAuthId())
.doOnSuccess(findAuthConfig -> {
if(findAuthConfig == null) {
return;
}
OAuth2RequestContext oAuth2RequestContext = new OAuth2RequestContext(orgId, null, null);
oAuth2RequestContext.setAuthConfig(findAuthConfig.authConfig());
AuthRequest authRequest = authRequestFactory.build(oAuth2RequestContext).block();
try {
AuthUser authUser = authRequest.refresh(connection.getAuthConnectionAuthToken().getRefreshToken()).block();
authUser.setAuthContext(oAuth2RequestContext);
authenticationApiService.updateConnection(authUser, user);
} catch (Exception e) {
log.error("Failed to refresh access token. Removing user sessions/tokens.");
tokensToRemove.addAll(connection.getTokens());
}
});
});
Optional<Connection> activeConnectionOptional = user.getConnections()
.stream()
.filter(connection -> connection.getAuthId().equals(user.getActiveAuthId()))
.findFirst();

if(!activeConnectionOptional.isPresent()) {
return Triple.of(user, activeConnection, orgId);
}

activeConnection = activeConnectionOptional.get();

if(!activeConnection.getAuthId().equals(DEFAULT_AUTH_CONFIG.getId())) {
if(activeConnection.getAuthConnectionAuthToken().getExpireAt() == 0) {
return Triple.of(user, activeConnection, orgId);
}
boolean isAccessTokenExpired = (activeConnection.getAuthConnectionAuthToken().getExpireAt()*1000) < Instant.now().toEpochMilli();
if(isAccessTokenExpired) {

Optional<String> orgIdOptional = activeConnection.getOrgIds().stream().findFirst();
if(!orgIdOptional.isPresent()) {
return Triple.of(user, activeConnection, orgId);
}
orgId = orgIdOptional.get();
}
});
}

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

})
}).flatMap(this::refreshOauthToken)
.flatMap(user -> chain.filter(exchange).contextWrite(withAuthentication(toAuthentication(user)))
.then(service.extendValidity(cookieToken))
);
}

private Mono<User> refreshOauthToken(Triple<User, Connection, String> triple) {

User user = triple.getLeft();
Connection connection = triple.getMiddle();
String orgId = triple.getRight();

if (connection == null || orgId == null) {
return Mono.just(user);
}

OAuth2RequestContext oAuth2RequestContext = new OAuth2RequestContext(triple.getRight(), null, null);

return authenticationService
.findAuthConfigByAuthId(orgId, connection.getAuthId())
.switchIfEmpty(Mono.empty())
.flatMap(findAuthConfig -> {

Mono<AuthRequest> authRequestMono = Mono.empty();

if(findAuthConfig == null) {
return authRequestMono;
}
oAuth2RequestContext.setAuthConfig(findAuthConfig.authConfig());

return authRequestFactory.build(oAuth2RequestContext);
}).flatMap(authRequest -> {
if(authRequest == null) {
return Mono.just(user);
}
try {
AuthUser authUser = authRequest.refresh(connection.getAuthConnectionAuthToken().getRefreshToken()).block();
authUser.setAuthContext(oAuth2RequestContext);
authenticationApiService.updateConnection(authUser, user);
return userService.update(user.getId(), user);
} catch (Exception e) {
log.error("Failed to refresh access token. Removing user sessions/tokens.");
connection.getTokens().forEach(token -> {
service.removeUserSession(token).block();
});
}
return Mono.just(user);
});

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.lowcoder.domain.authentication.AuthenticationService;
import org.lowcoder.domain.authentication.context.AuthRequestContext;
import org.lowcoder.domain.user.model.User;
import org.lowcoder.domain.user.service.UserService;
import org.lowcoder.infra.constant.NewUrl;
import org.lowcoder.sdk.config.CommonConfig;
import org.lowcoder.sdk.util.CookieHelper;
Expand Down Expand Up @@ -50,6 +51,9 @@ public class SecurityConfig {
@Autowired
private SessionUserService sessionUserService;

@Autowired
private UserService userService;

@Autowired
private AccessDeniedHandler accessDeniedHandler;

Expand Down Expand Up @@ -153,7 +157,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
.accessDeniedHandler(accessDeniedHandler)
);

http.addFilterBefore(new UserSessionPersistenceFilter(sessionUserService, cookieHelper, authenticationService, authenticationApiService, authRequestFactory), SecurityWebFiltersOrder.AUTHENTICATION);
http.addFilterBefore(new UserSessionPersistenceFilter(sessionUserService, userService, cookieHelper, authenticationService, authenticationApiService, authRequestFactory), SecurityWebFiltersOrder.AUTHENTICATION);
http.addFilterBefore(new APIKeyAuthFilter(sessionUserService, cookieHelper, jwtUtils), SecurityWebFiltersOrder.AUTHENTICATION);

return http.build();
Expand Down