update JWT handling for external services

Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
2024-08-28 22:37:20 +02:00
parent 2566897b06
commit efd6be775e
5 changed files with 101 additions and 7 deletions

View File

@@ -38,6 +38,8 @@ dependencies {
implementation 'com.google.cloud:spring-cloud-gcp-starter' implementation 'com.google.cloud:spring-cloud-gcp-starter'
implementation 'com.google.auth:google-auth-library-oauth2-http' implementation 'com.google.auth:google-auth-library-oauth2-http'
implementation 'io.grpc:grpc-netty' implementation 'io.grpc:grpc-netty'
implementation 'io.netty:netty-all'
implementation 'com.nimbusds:nimbus-jose-jwt:9.40'
testImplementation 'io.projectreactor:reactor-test' testImplementation 'io.projectreactor:reactor-test'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools' developmentOnly 'org.springframework.boot:spring-boot-devtools'

View File

@@ -0,0 +1,76 @@
package dev.mars3142.fhq.edge;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import dev.mars3142.fhq.edge.exceptions.AuthUnauthorizedException;
import java.net.URI;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class AuthGatewayFilter extends AbstractGatewayFilterFactory<AuthGatewayFilter.Config> {
public AuthGatewayFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
val bearer = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (bearer == null) {
log.error("No authorization header");
throw new AuthUnauthorizedException();
}
val token = bearer.substring("Bearer ".length());
if (token.isEmpty()) {
log.error("Empty token");
throw new AuthUnauthorizedException();
}
try {
val claimsSet = verifyToken(config, token);
exchange.getRequest()
.mutate()
.header("x-user-id", claimsSet.getStringClaim("user_id"));
return chain.filter(exchange);
} catch (Exception e) {
log.error("Error while verifying token", e);
throw new AuthUnauthorizedException();
}
};
}
private JWTClaimsSet verifyToken(Config config, String token) throws Exception {
val url = new URI(
"https://www.googleapis.com/service_accounts/v1/jwk/securetoken%40system.gserviceaccount.com").toURL();
val keySource = JWKSourceBuilder.create(url).build();
val algorithm = JWSAlgorithm.RS256;
val selector = new JWSVerificationKeySelector<>(algorithm, keySource);
val jwtClaimsSet = new JWTClaimsSet.Builder().issuer("https://securetoken.google.com/firmware-hq")
.audience(config.audience).build();
val claimsVerifier = new DefaultJWTClaimsVerifier<>(config.audience, jwtClaimsSet, null);
claimsVerifier.verify(jwtClaimsSet, null);
val processor = new DefaultJWTProcessor<>();
processor.setJWSKeySelector(selector);
processor.setJWTClaimsSetVerifier(claimsVerifier);
return processor.process(token, null);
}
@Setter
public static class Config {
private String audience;
}
}

View File

@@ -44,14 +44,13 @@ public class GCPGatewayFilter extends AbstractGatewayFilterFactory<GCPGatewayFil
val tokenCredential = val tokenCredential =
IdTokenCredentials.newBuilder() IdTokenCredentials.newBuilder()
.setIdTokenProvider((IdTokenProvider) credentials) .setIdTokenProvider((IdTokenProvider) credentials)
.setTargetAudience(config.getAudience()) .setTargetAudience(config.audience)
.build(); .build();
return tokenCredential.refreshAccessToken().getTokenValue(); return tokenCredential.refreshAccessToken().getTokenValue();
} }
@Setter @Setter
@Getter
public static class Config { public static class Config {
private String audience; private String audience;

View File

@@ -0,0 +1,9 @@
package dev.mars3142.fhq.edge.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class AuthUnauthorizedException extends RuntimeException {
}

View File

@@ -35,15 +35,23 @@ spring:
filters: filters:
- name: GCPGatewayFilter - name: GCPGatewayFilter
args: args:
audience: firmware-hq audience: ${TIMEZONE_SERVICE_URI:http://timezone-service}
- id: backend-service - id: account-service
uri: ${BACKEND_SERVICE_URI:http://backend-service} uri: ${BACKEND_SERVICE_URI:http://backend-service}
predicates: predicates:
- Path=/v1/account/** - Path=/v1/account/**
- Path=/v1/token/**
filters: filters:
#- AuthGatewayFilter
- name: GCPGatewayFilter - name: GCPGatewayFilter
args: args:
audience: firmware-hq audience: ${BACKEND_SERVICE_URI:http://backend-service}
- id: token-service
uri: ${BACKEND_SERVICE_URI:http://backend-service}
predicates:
- Path=/v1/token/**
filters:
- AuthGatewayFilter
- name: GCPGatewayFilter
args:
audience: ${BACKEND_SERVICE_URI:http://backend-service}