Skip to content

Commit

Permalink
fix: #202 fix logout, add gateway bearer filter, optimize imports
Browse files Browse the repository at this point in the history
  • Loading branch information
KartVen committed Dec 1, 2024
1 parent 7e166b6 commit 673cb99
Show file tree
Hide file tree
Showing 55 changed files with 507 additions and 502 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,14 @@ private String generate(
public Try<Token.Deserialize> parseToken(String token) {
return parseClaims(token)
.mapTry(claims -> {
var bearerId = claims.get(ClaimKey.BEARER_ID, UUID.class);
var id = claims.get(ClaimKey.ID, Long.class);
var username = claims.get(ClaimKey.USERNAME, String.class);
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) claims.get(ClaimKey.AUTHORITIES, List.class);
var authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
var state = Optional.ofNullable(claims.get(ClaimKey.STATE, Integer.class)).orElse(8);
return new Token.Deserialize(id, username, state, authorities);
return new Token.Deserialize(bearerId, id, username, state, authorities);
})
.onFailure(th -> log.error("Cannot parse token"));
}
Expand All @@ -131,6 +132,7 @@ private static class ClaimKey {
static String USERNAME = Claims.SUBJECT;
static String STATE = "state";
static String AUTHORITIES = "authorities";
static String BEARER_ID = Claims.ID;
}

@Getter
Expand Down Expand Up @@ -171,6 +173,7 @@ public record Token(UUID id, String value, Date expiration) {
@Getter
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public static class Deserialize {
UUID bearerId;
Long id;
String username;
boolean isExpired;
Expand All @@ -179,7 +182,8 @@ public static class Deserialize {
boolean isEnabled;
Set<SimpleGrantedAuthority> authorities;

public Deserialize(Long id, String username, Integer state, Set<SimpleGrantedAuthority> authorities) {
public Deserialize(UUID bearerId, Long id, String username, Integer state, Set<SimpleGrantedAuthority> authorities) {
this.bearerId = bearerId;
this.id = id;
this.username = username;
if (state != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
package pl.sknikod.kodemygateway.infrastructure.module.oauth2;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@Component
Expand Down Expand Up @@ -47,29 +42,11 @@ private Authentication toOAuth2LoginAuthenticationToken(OAuth2AuthorizationCodeA
return new OAuth2LoginAuthenticationToken(
token.getClientRegistration(),
token.getAuthorizationExchange(),
emptyOAuth2User(),
Collections.emptyList(),
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "gateway_token_value", null, null),
new DefaultOAuth2User(token.getAuthorities(), Map.of("name", token.getName()), "name"),
token.getAuthorities(),
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token.getAccessToken().getTokenValue(),
token.getAccessToken().getIssuedAt(), token.getAccessToken().getExpiresAt()),
token.getRefreshToken()
);
}

private OAuth2User emptyOAuth2User() {
return new OAuth2User() {
@Override
public Map<String, Object> getAttributes() {
return Map.of();
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}

@Override
public String getName() {
return OAuth2User.class.getSimpleName();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package pl.sknikod.kodemygateway.util;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Optional;

@Component
@Slf4j
public class AddAuthorizationGatewayFilterFactory
extends AbstractGatewayFilterFactory<Object> {

public AddAuthorizationGatewayFilterFactory() {
super(Object.class);
}

@Override
public GatewayFilter apply(Object config) {
return new AddAuthorizationFilter();
}

@RequiredArgsConstructor
private static final class AddAuthorizationFilter implements GatewayFilter {
private static final String ACCESS_TOKEN_COOKIE = "AUTH_CONTEXT";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return Optional.ofNullable(exchange.getRequest().getCookies().getFirst(ACCESS_TOKEN_COOKIE))
.map(HttpCookie::getValue)
.filter(Strings::isNotEmpty)
.map(token -> {
log.info("Adding {} header", HttpHeaders.AUTHORIZATION);
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.setBearerAuth(token))
.header(HttpHeaders.COOKIE, (String) null)
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
})
.orElseGet(() -> chain.filter(exchange));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package pl.sknikod.kodemygateway.util;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpResponse;
Expand All @@ -18,12 +17,15 @@
import reactor.netty.Connection;
import reactor.netty.DisposableChannel;

import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Optional;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_CONN_ATTR;

@Component
@Slf4j
public class FrontendRedirectGatewayFilterFactory
extends AbstractGatewayFilterFactory<FrontendRedirectGatewayFilterFactory.Config> {

Expand All @@ -44,7 +46,12 @@ public static class Config {
private String location;
}

private record FrontendRedirectGatewayFilter(Config config) implements GatewayFilter, Ordered {
@RequiredArgsConstructor
private static final class FrontendRedirectGatewayFilter implements GatewayFilter, Ordered {
private final Config config;
private static final String ACCESS_TOKEN_COOKIE = "AUTH_CONTEXT";
private static final String REFRESH_TOKEN_COOKIE = "AUTH_PERSIST";

@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER;
Expand All @@ -55,9 +62,8 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.then(Mono.<Void>defer(() -> {
if (exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR) != null) {
ServerHttpResponse response = exchange.getResponse();
addCookieToResponse(response);
performRedirect(response);
modifyHeaders(exchange.getResponse());
performRedirect(exchange.getResponse());
disposeConnection(exchange);
}
return Mono.empty();
Expand All @@ -66,18 +72,49 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
.doOnError(th -> disposeConnection(exchange));
}

private void addCookieToResponse(ServerHttpResponse response) {
response.addCookie(ResponseCookie.from(" ", "test")
private void modifyHeaders(ServerHttpResponse response) {
if (response.getStatusCode() != HttpStatus.OK) {
log.warn("Response status code is {}. Skipping modifyHeaders", response.getStatusCode());
return;
}

HttpHeaders headers = response.getHeaders();
List<String> accessTokens = headers.get(ACCESS_TOKEN_COOKIE);
List<String> refreshTokens = headers.get(REFRESH_TOKEN_COOKIE);

if (accessTokens == null || accessTokens.isEmpty()
|| refreshTokens == null || refreshTokens.isEmpty()) {
log.warn("Authorization tokens don't exist or are empty. Skipping modifyHeaders");
return;
}

var accessToken = createCookie(
ACCESS_TOKEN_COOKIE,
"eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJwbC5za25pa29kLmtvZGVteSIsImp0aSI6ImVhM2FiMDljLTU5ZGQtNGI1Zi04OGVkLTk3YzNiZmRhNmMyNiIsInN1YiI6IkthcnRWZW4iLCJpYXQiOjE3MzMwNjY3MjYsImV4cCI6MTczMzEwOTkyNiwiaWQiOjEsImF1dGhvcml0aWVzIjpbIkNBTl9BVVRPX0FQUFJPVkVEX01BVEVSSUFMIiwiQ0FOX0RFUFJFQ0FURV9NQVRFUklBTCIsIkNBTl9VTkJBTl9NQVRFUklBTCIsIkNBTl9CQU5fTUFURVJJQUwiLCJDQU5fTU9ESUZZX1RBR1MiLCJDQU5fUkVBRF9OT1RJRklDQVRJT05TIiwiQ0FOX0JBTk5JTkdfVVNFUlMiLCJDQU5fR0VUX1VTRVJfSU5GTyIsIkNBTl9HRVRfVVNFUlMiLCJDQU5fSU5ERVgiLCJDQU5fVklFV19BTExfTUFURVJJQUxTIiwiQ0FOX0FTU0lHTl9ST0xFUyIsIkNBTl9BUFBST1ZFRF9NQVRFUklBTCJdLCJzdGF0ZSI6OH0.FTQheX7efGI4Ie2IqZy_ZVOdvWw9HZIfQYDvdtqwuCrTguL7QLiGLTA0HDif3Xu",
Duration.ofDays(1)
);

var refreshToken = createCookie(
REFRESH_TOKEN_COOKIE,
"16fcb136-fd3c-490c-b151-184c07d3871e",
Duration.ofDays(1)
);

headers.addAll(HttpHeaders.SET_COOKIE, List.of(accessToken.toString(), refreshToken.toString()));
}

private ResponseCookie createCookie(@NonNull String name, @NonNull String value, @NonNull Duration age) {
return ResponseCookie.from(name, value)
.path("/")
.httpOnly(true)
.secure(true)
.maxAge(Duration.ofDays(1))
.build());
.sameSite("Lax")
.maxAge(age)
.build();
}

private void performRedirect(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.FOUND);
response.getHeaders().setLocation(java.net.URI.create(config.location));
response.getHeaders().setLocation(URI.create(config.location));
}

private void disposeConnection(ServerWebExchange exchange) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package pl.sknikod.kodemygateway.util;

import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_CONN_ATTR;

@Component
@Slf4j
public class LogoutGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

public LogoutGatewayFilterFactory() {
super(Object.class);
}

@Override
public GatewayFilter apply(Object config) {
return new LogoutGatewayFilter();
}

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Config {
private String location;
}

@RequiredArgsConstructor
private static final class LogoutGatewayFilter implements GatewayFilter, Ordered {
private static final String ACCESS_TOKEN_COOKIE = "AUTH_CONTEXT";
private static final String REFRESH_TOKEN_COOKIE = "AUTH_PERSIST";

@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.then(Mono.<Void>defer(() -> {
if (exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR) != null) {
modifyHeaders(exchange.getResponse());
}
return Mono.empty();
}));
}

private void modifyHeaders(ServerHttpResponse response) {
if (response.getStatusCode() != HttpStatus.OK) {
log.warn("Response status code is {}. Execute modifyHeaders anyway", response.getStatusCode());
}
response.getHeaders().addAll(
HttpHeaders.SET_COOKIE,
List.of(createExpiredCookie(ACCESS_TOKEN_COOKIE).toString(), createExpiredCookie(REFRESH_TOKEN_COOKIE).toString())
);
}

private ResponseCookie createExpiredCookie(String name) {
return ResponseCookie.from(name).maxAge(0).build();
}
}
}
Loading

0 comments on commit 673cb99

Please sign in to comment.