Skip to content

Commit

Permalink
Merge pull request #203 from skni-kod/issue-202
Browse files Browse the repository at this point in the history
Poprawka wylogowania i oauth2
  • Loading branch information
KartVen authored Dec 1, 2024
2 parents b18deba + 673cb99 commit 3c08468
Show file tree
Hide file tree
Showing 80 changed files with 1,093 additions and 818 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dev.kodemy.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ jobs:
export DATASOURCE_URL=jdbc:postgresql://$DATASOURCE_CONTAINER/$DATASOURCE_DATABASE
cd $WORKING_DIRECTORY
docker compose -f docker-compose.app.yml up --build -d
docker compose -f docker-compose.app.yml -f docker-compose.app.expose.yml up --build -d
- name: Cleanup
uses: appleboy/ssh-action@master
with:
Expand Down
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
Expand Up @@ -20,7 +20,7 @@ public class JwtConfiguration {
@NoArgsConstructor
@ConfigurationProperties(prefix = "jwt")
public static class JwtProperties {
private String secretKey;
private String secretKey = "";
private Bearer bearer = new Bearer();
private Delegation delegation = new Delegation();

Expand Down
24 changes: 24 additions & 0 deletions docker-compose.app.expose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
services:
kodemy-service-registry:
ports:
- "8761:8761"

kodemy-api-gateway:
ports:
- "8080:8080"

kodemy-auth:
ports:
- "8081:8080"

kodemy-backend:
ports:
- "8082:8080"

kodemy-notification:
ports:
- "8084:8080"

kodemy-search:
ports:
- "8083:8080"
15 changes: 3 additions & 12 deletions docker-compose.app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ services:
build:
context: ./kodemy-service-registry
dockerfile: ./Dockerfile
ports:
- "8761:8761"
networks:
- kodemy
deploy:
Expand Down Expand Up @@ -35,8 +33,6 @@ services:
dockerfile: ./Dockerfile
networks:
- kodemy
ports:
- "8081:8080"
environment:
SPRING_DATASOURCE_URL: ${DATASOURCE_URL}?currentSchema=kodemy-auth
SPRING_DATASOURCE_USERNAME: ${DATASOURCE_USERNAME}
Expand All @@ -56,8 +52,6 @@ services:
dockerfile: ./Dockerfile
networks:
- kodemy
ports:
- "8082:8080"
environment:
SPRING_DATASOURCE_URL: ${DATASOURCE_URL}?currentSchema=kodemy-backend
SPRING_DATASOURCE_USERNAME: ${DATASOURCE_USERNAME}
Expand All @@ -77,8 +71,6 @@ services:
dockerfile: ./Dockerfile
networks:
- kodemy
ports:
- "8084:8080"
environment:
SPRING_DATASOURCE_URL: ${DATASOURCE_URL}?currentSchema=kodemy-backend
SPRING_DATASOURCE_USERNAME: ${DATASOURCE_USERNAME}
Expand All @@ -98,8 +90,6 @@ services:
dockerfile: ./Dockerfile
networks:
- kodemy
ports:
- "8083:8080"
environment:
SPRING_RABBITMQ_HOST: ${RABBITMQ_HOST}
SPRING_RABBITMQ_PORT: ${RABBITMQ_PORT}
Expand All @@ -108,6 +98,7 @@ services:
EUREKA_URL: ${EUREKA_URL}
deploy:
replicas: 1

networks:
kodemy:
kodemy:
driver: bridge
1 change: 1 addition & 0 deletions kodemy-api-gateway/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ext {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package pl.sknikod.kodemygateway.configuration;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import pl.sknikod.kodemygateway.infrastructure.module.oauth2.OAuth2ReactiveAuthorizationManager;
import reactor.core.publisher.Mono;

import java.util.function.Function;

import static org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizationRequestResolver.DEFAULT_REGISTRATION_ID_URI_VARIABLE_NAME;

@Configuration
@Slf4j
@EnableWebFluxSecurity
public class SecurityConfiguration {
private static final Function<String, ServerWebExchangeMatcher> PATH_MATCHER_FUNCTION;

static {
PATH_MATCHER_FUNCTION = (endpoint) -> new PathPatternParserServerWebExchangeMatcher(
endpoint + "/{" + DEFAULT_REGISTRATION_ID_URI_VARIABLE_NAME + "}"
);
}

@Bean
public SecurityWebFilterChain springSecurityFilterChain(
ServerHttpSecurity http,
ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver,
@Value("${app.security.oauth2.endpoint.callback}") String callbackEndpoint,
OAuth2ReactiveAuthorizationManager reactiveAuthenticationManager
) {
http
.authorizeExchange(auth -> auth.anyExchange().permitAll())
.oauth2Login(oauth2 -> oauth2
.authorizationRequestResolver(authorizationRequestResolver)
.authenticationMatcher(callbackMatcher(callbackEndpoint))
.authenticationManager(reactiveAuthenticationManager)
.authenticationSuccessHandler(authenticationSuccessHandler())
// TODO check if need change
//.authenticationFailureHandler(authenticationFailureHandler())
);
return http.build();
}

private ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
return (webFilterExchange, authentication) -> webFilterExchange
.getChain()
.filter(webFilterExchange.getExchange())
.and(Mono.empty());
}

/*private ServerAuthenticationFailureHandler authenticationFailureHandler() {
return (webFilterExchange, exception) -> Mono.empty();
}*/

@Bean
public ServerOAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver(
ReactiveClientRegistrationRepository clientRegistrationRepository,
@Value("${app.security.oauth2.endpoint.authorize}") String authorizeEndpoint
) {
return new DefaultServerOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, PATH_MATCHER_FUNCTION.apply(authorizeEndpoint)
);
}

public ServerWebExchangeMatcher callbackMatcher(@NonNull String callbackEndpoint) {
return PATH_MATCHER_FUNCTION.apply(callbackEndpoint);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package pl.sknikod.kodemygateway.infrastructure.module.oauth2;

import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
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.DefaultOAuth2User;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Map;

@Component
public class OAuth2ReactiveAuthorizationManager implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.defer(() -> {
final var token = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
final var exchange = token.getAuthorizationExchange();
if (exchange.getAuthorizationResponse().statusError()) {
return Mono.error(new OAuth2AuthorizationException(exchange.getAuthorizationResponse().getError()));
}
if (!isStateEqually(exchange)) {
return Mono.error(new OAuth2AuthorizationException(new OAuth2Error("invalid_state_parameter")));
}
return Mono.just(toOAuth2LoginAuthenticationToken(token)).onErrorMap(OAuth2AuthorizationException.class,
(e) -> new OAuth2AuthenticationException(e.getError(), e.getError().toString(), e));
});
}

private boolean isStateEqually(OAuth2AuthorizationExchange exchange) {
return exchange.getAuthorizationRequest().getState()
.equals(exchange.getAuthorizationResponse().getState());
}

private Authentication toOAuth2LoginAuthenticationToken(OAuth2AuthorizationCodeAuthenticationToken token) {
return new OAuth2LoginAuthenticationToken(
token.getClientRegistration(),
token.getAuthorizationExchange(),
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()
);
}
}
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));
}
}
}
Loading

0 comments on commit 3c08468

Please sign in to comment.