Spring Cloud Gateway altering form data does not work - reactive-programming

I defined this GatewayFilter:
EDIT More context information:
What I would like to achieve is to avoid the client providing its credentials to get an access token from an authorization server.
The client sends a POST request with user's credentials (username/password) and the gateway adds all complementary information like scope, client_id, grant_type etc... before forwarding the request to the authorization server.
#Component
public class OAuth2CredentialsAppenderGatewayFilterFactory extends AbstractGatewayFilterFactory<OAuth2CredentialsAppenderGatewayFilterFactory.Config> {
public OAuth2CredentialsAppenderGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
if ("x-www-form-urlencoded".equals(request.getHeaders().getContentType().getSubtype())) {
//This code is not executed, the call of formData.put does not do anything, even a breakpoint is not reached!
if (request.getMethod().equals(HttpMethod.POST)) {
exchange.getFormData().map(formData -> {
formData.put("key1", List.of("value1"));
formData.put("key2", List.of("value2"));
formData.put("key3", List.of("value3"));
return formData;
});
}
//This part of code works well, the header is added to the forwarded request
requestBuilder.header(HttpHeaders.AUTHORIZATION,
"Basic " + Base64Utils.encodeToString((this.uiClientId + ":" + this.uiClientSecret).getBytes()));
}
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
};
}
}
I use the filter like this:
- id: keycloak_token_route
uri: http://localhost:8180
predicates:
- Path=/kc/token
filters:
- OAuth2CredentialsAppender
- SetPath=/auth/realms/main/protocol/openid-connect/token
- name: RequestRateLimiter
args:
key-resolver: "#{#userIpKeyResolver}"
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 30
denyEmptyKey: false
The filter is well invoked but altering the incoming request body does not work.
I am new to the reactive world so I am a bit confused, any help will be appreciated.

For those who would like to do the same thing, this is how I solved my problem. Again I am not an expert of Reactive programming, I am still learning it so it might be a better answer.
#Component
public class OAuth2CredentialsAppenderGatewayFilterFactory extends AbstractGatewayFilterFactory<OAuth2CredentialsAppenderGatewayFilterFactory.Config> {
#Value("${uiservice.clientId}")
private String uiClientId;
#Value("${uiservice.clientSecret}")
private String uiClientSecret;
public OAuth2CredentialsAppenderGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (ServerWebExchange exchange, GatewayFilterChain chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
if (nonNull(request.getHeaders().getContentType()) && request.getHeaders().getContentType().equals(MediaType.APPLICATION_FORM_URLENCODED)) {
if (requireNonNull(request.getMethod()).equals(HttpMethod.POST)) {
//Use this filter to modify the request body
ModifyRequestBodyGatewayFilterFactory.Config requestConf = new ModifyRequestBodyGatewayFilterFactory.Config()
.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.setRewriteFunction(String.class, String.class, this.completeRequestBody());
requestBuilder.header(HttpHeaders.AUTHORIZATION, base64Encoding(this.uiClientId, this.uiClientSecret));
return new ModifyRequestBodyGatewayFilterFactory().apply(requestConf).filter(exchange.mutate().request(requestBuilder.build()).build(), chain);
}
}
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
};
}
/** Add some config params if needed */
public static class Config {
}
/** Complete request by adding required information to get the access token. Here we can get 2 type of token: client_credentials or password. If the param client_only=true we should get a client_credentials token */
private RewriteFunction<String, String> completeRequestBody() {
return (ServerWebExchange ex, String requestBody) -> {
requireNonNull(requestBody, "Body is required");
//if body contains only this, we should get a client_credentials token
var idForClientCredentialsOnly = "client=ui&client_only=true";
String finalRequestBody;
var joiner = new StringJoiner("");
if (idForClientCredentialsOnly.equalsIgnoreCase(requestBody)) {
joiner.add("grant_type=").add("client_credentials");
}
else {
joiner.add(requestBody);
if (!containsIgnoreCase(requestBody, "grant_type")) {
joiner.add("&grant_type=").add("password");
}
}
if (!containsIgnoreCase(requestBody, "scope")) {
joiner.add("&scope=").add("uiclient");//I use Keycloak so I specify the scope to get some extra information
}
finalRequestBody = joiner.toString();
return Mono.just(isBlank(finalRequestBody) ? requestBody : finalRequestBody);
};
}
}

Related

Keycloak - Customize "sub" format in JWT token

I'm trying to find a way to change the "sub" format in JWT Token provided by Keycloak, I know it came from Keycloak User Id but i'm not sure we can't change it.
For example for now I have something like this :
"sub": "f:39989175-b393-4fad-8f84-628b9712f93b:testldap",
I would like it smaller 😅.
I'm not sure that modifying 'sub' is a good idea, but if you sure, you can use something like that:
/**
* Class for signing JWT (when you get tokens in base64 actually they are
* signed by issuer server see https://jwt.io)
*/
public static class JwtSigner {
private final KeyPair keyPair;
private final String kid;
public JwtSigner(String privateKeyPem) {
PrivateKey privateKey = PemUtils.decodePrivateKey(privateKeyPem);
PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
keyPair = new KeyPair(publicKey, privateKey);
kid = KeyUtils.createKeyId(keyPair.getPublic());
}
public String encodeToken(AccessToken accessToken) {
return new JWSBuilder()
.type("JWT")
.kid(kid)
.jsonContent(accessToken)
.sign(Algorithm.RS256, keyPair.getPrivate());
}
}
/**
* This class allows you to update several token fields and re-encode token
*/
public static class JwtTransformer<T extends AccessToken> {
private T token;
public JwtTransformer(String tokenString, Class<T> tokenType) throws JWSInputException {
try {
token = JsonSerialization.readValue(new JWSInput(tokenString).getContent(), tokenType);
} catch (IOException e) {
throw new JWSInputException(e);
}
}
public static <T extends AccessToken> T decode(String tokenString, Class<T> tokenType) throws JWSInputException {
return new JwtTransformer<>(tokenString, tokenType).decode();
}
public static JwtTransformer<AccessToken> forAccessToken(String tokenString) throws JWSInputException {
return new JwtTransformer<>(tokenString, AccessToken.class);
}
public static JwtTransformer<RefreshToken> forRefreshToken(String tokenString) throws JWSInputException {
return new JwtTransformer<>(tokenString, RefreshToken.class);
}
public T decode() {
return token;
}
public JwtTransformer transform(Consumer<T> consumer) {
consumer.accept(token);
return this;
}
public String encode(JwtSigner jwtSigner) {
return jwtSigner.encodeToken(token);
}
}
I used this classes for tests, but you can adopt them for your needs. Take a note that private key that required for JwtSigner initializaton is stored in keycloak DB, and can not be easily extracted via Admin Console UI. Check out result of
select VALUE
from KEYCLOAK.COMPONENT
inner join KEYCLOAK.COMPONENT_CONFIG
on KEYCLOAK.COMPONENT.ID = KEYCLOAK.COMPONENT_CONFIG.COMPONENT_ID
where PARENT_ID = '%YOUR_REALM_NAME%'
and PROVIDER_ID = 'rsa-generated'
and COMPONENT_CONFIG.NAME = 'privateKey';
So finally you can do something like
String new AccessToken = JwtTransformer.forAccessToken(accessTokenString)
.transform(token -> {
token.subject(subModificationFunction(token.getSubject()))
})
.encode();

How can we conditionally route to a different URL in Spring Cloud Gateway? Is there a reference sample?

Trying to change the exchange target URL conditionally. Is there a way this can be achieved in Spring Cloud Gateway?
To elaborate, upon inspecting a particular cookie value in the incoming request, I would like to route it to a different URL.
We do something similar with request headers here. We have an abstract filter that handles setting the uri correctly, you just have to determine the uri from the ServerWebExchange.
public class CookieToRequestUriGatewayFilterFactory extends
AbstractChangeRequestUriGatewayFilterFactory<AbstractGatewayFilterFactory.NameConfig> {
private final Logger log = LoggerFactory
.getLogger(RequestHeaderToRequestUriGatewayFilterFactory.class);
public RequestHeaderToRequestUriGatewayFilterFactory() {
super(NameConfig.class);
}
#Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY);
}
#Override
protected Optional<URI> determineRequestUri(ServerWebExchange exchange,
NameConfig config) {
String cookieValue = exchange.getRequest().getCookies().getFirst(config.getName());
String requestUrl = determineUrlFromCookie(cookieValue);
return Optional.ofNullable(requestUrl).map(url -> {
try {
return new URL(url).toURI();
}
catch (MalformedURLException | URISyntaxException e) {
log.info("Request url is invalid : url={}, error={}", requestUrl,
e.getMessage());
return null;
}
});
}
}
It would be up to you to implement determineUrlFromCookie().

How do I extract information from an incoming JWT that was generated by an external service?

How do I extract information from an incoming JWT that was generated by an external service? (Okta)
I need to perform a database lookup of user information based on one of the fields in the JWT. (I also want method-level security based on the scope of the JWT.)
The secret seems to be in using an AccessTokenConverter to extractAuthentication() and then use that to lookup UserDetails. I am stuck because every example I can find includes setting up an Authorization Server, which I don't have, and I can't tell if the JwtAccessTokenConverter will work on the Resource Server.
My resource server runs and handles requests, but my custom JwtAccessTokenConverter is never getting called during incoming requests;
All of my requests are coming in with a principal of anonymousUser.
I am using Spring 5.1.1.
My Resource Server Configuration
#Configuration
#EnableResourceServer
public class OauthResourceConfig extends ResourceServerConfigurerAdapter {
#Value("${oauth2.audience}")
String audience;
#Value("${oauth2.baseUrl}/v1/keys")
String jwksUrl;
#Override
public void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/api/**").permitAll();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.tokenServices(tokenServices())
.resourceId(audience);
}
#Primary
#Bean
public DefaultTokenServices tokenServices() throws Exception {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
return tokenServices;
}
#Bean
public TokenStore tokenStore() {
return new JwkTokenStore(jwksUrl, accessTokenConverter());
}
#Bean
public AccessTokenConverter accessTokenConverter() {
return new CustomJwtAccessTokenConverter();
}
}
My Custom Access Token Converter
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
LinkedHashMap userDetails = (LinkedHashMap) map.get("userDetails");
if (userDetails != null) {
... Do the database lookup here ...
Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();
userAuthentication = new UsernamePasswordAuthenticationToken(extendedPrincipal,
userAuthentication.getCredentials(), authorities);
}
}
return new OAuth2Authentication(authentication.getOAuth2Request(), userAuthentication);
}
}
And my Resource
#GET
#PreAuthorize("#oauth2.hasScope('openid')")
public Response getRecallsByVin(#QueryParam("vin") String vin,
#QueryParam("page") Integer pageNumber,
#QueryParam("pageSize") Integer pageSize) {
List<VehicleNhtsaCampaign> nhtsaCampaignList;
List<OpenRecallsDto> nhtsaCampaignDtoList;
SecurityContext securityContext = SecurityContextHolder.getContext();
Object principal = securityContext.getAuthentication().getPrincipal();
... More irrelevant code follows ...
First of all, the #PreAuthorize annotation isn't doing anything. If I change it to #PreAuthorize("#oauth2.hasScope('FooBar')") it still lets the request in.
Secondly, I need to grab other information off the JWT so I can do a user lookup in my database. I thought that by adding the accessTokenConverter() in the resource server config, the JWT would be parsed and placed into the securityContext.getAuthentication() response. Instead all I'm getting is "anonymousUser".
UPDATE: I later found out the data I need is coming in a custom header, so I don't need to extract anything from the JWT. I was never able to validate any of the suggested answers.
Are you using Spring Boot?
The Spring Security 5.1 has support for JWT access tokens. For example, you could just supply a new JwtDecoder:
https://github.com/okta/okta-spring-boot/blob/spring-boot-2.1/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2ResourceServerAutoConfig.java#L62-L84
You can create a filter that validates and sets token to SecurityContextHolder. This is what I have done in my project using jsonwebtoken dependency:
public class JWTFilter extends GenericFilterBean {
private String secretKey = 'yoursecret';
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = resolveToken(httpServletRequest);
if (validateToken(jwt)) {
Authentication authentication = getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private String resolveToken(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
} catch (MalformedJwtException e) {
} catch (ExpiredJwtException e) {
} catch (UnsupportedJwtException e) {
} catch (IllegalArgumentException e) {
}
return false;
}
}
You can then access your token from SecurityContextHolder.
For cleaner way to access token fields, I have created POJO models of my token from http://www.jsonschema2pojo.org/

Web API 2 use Windows Authentication for public users

How do I use Windows Authentication in WEB API for internal users who will also be on the public network? The REST API will be public facing and will need to authenticate intranet users as well as internet users. Basically, anybody not on Active Directory won't be able to access it and one more AD groups will be authorized.
The REST service at the moment has a security filter to validate token using attribute filter.
public class RestAuthorizeAttribute : AuthorizeAttribute
{
private const string SecurityToken = "token";
public override void OnAuthorization(HttpActionContext actionContext)
{
if (Authorize(actionContext))
{
return;
}
HandleUnauthorizedRequest(actionContext);
}
private bool Authorize(HttpActionContext actionContext)
{
try
{
HttpRequestMessage request = actionContext.Request;
//Extract Token from the Request. This will work for all.
// E.g \api\Facilitiles\Token\298374u23lknndsjlkfds==
// \api\Ward\123\Token\298374u23lknndsjlkfds==
string path = request.RequestUri.LocalPath;
int indexOfToken = path.IndexOf(SecurityToken) + SecurityToken.Length + 1;
string token = path.Substring(indexOfToken);
bool isValid = SecurityManager.IsTokenValid(token, IpResolver.GetIp(request),request.Headers.UserAgent.ToString());
return isValid;
}
catch (Exception ex)
{
string av = ex.Message;
return false;
}
}
}
This is then applied to specific controllers like this:
[RestAuthorize]
[RoutePrefix("api/patient")]
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class PatientDetailsController : ApiController
{
PatientDetailsRetriever _patientDetailsRetriever;
// GET: api/patient/meds/personId/{personId}/token/{token}
[Route("meds/personId/{personId}/token/{token}")]
[HttpGet]
public HttpResponseMessage GetMeds(Int64 personId, string token)
{
List<Medication> meds;
.....
The client generates the token which includes username, password and domain and among other things.
Enabling Windows Authentication in IIS (web.config) will be enough to validate local users. But how does this work when the user is outside the network and sends in the credentials?
I have found the answer on this SO post.
//create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "YOURDOMAIN"))
{
// validate the credentials
bool isValid = pc.ValidateCredentials("myuser", "mypassword");
}

Securing a GWT app with a request param to be checked in a crosscontext attribute

My application is supposed to received a request parameter called sessionId which is supposed to be used to lookup for a crosscontext attribute.
I was looking at Spring Security to implement this and I think already have a good implementation of my AuthenticationProvider :
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
ServletContext servletContext = request.getSession().getServletContext();
String sessionId = request.getParameter("sessionId");
if (sessionId != null) {
ServletContext sc = request.getSession().getServletContext();
Object obj = sc.getContext("/crosscontext").getAttribute(sessionId);
if (obj != null) {
// return new Authentication
}
} else {
logger.error("No session id provided in the request");
return null;
}
if (!GWT.isProdMode()) {
// return new Authentication
} else {
logger.error("No session id provided in the request");
return null;
}
}
Now, what I would like to do is to configure Spring Security to not prompt for a user name and password, to let it reach this authentication provider call the authenticate method.
How can I achieve this ?
I fixed my issue by reviewing the design of my security and going for something closer to the preauthenticated mechanisms that are already provided by Spring Security.
I extended 2 components of Spring Security.
First one is an AbstractPreAuthenticatedProcessingFilter, usually his role is to provide the principal provided in the headers. In my case, I retrieve the header value and search in the context shared between 2 application for an attribute that corresponds to that header and returns it as principal :
public class MyApplicationPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
private static final Logger logger = Logger.getLogger(MyApplicationPreAuthenticatedProcessingFilter.class);
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
if (MyApplicationServerUtil.isProdMode()) {
String principal = request.getHeader("MY_HEADER");
String attribute = (String) request.getSession().getServletContext().getContext("/crosscontext").getAttribute(principal);
logger.info("In PROD mode - Found value in crosscontext: " + attribute);
return attribute;
} else {
logger.debug("In DEV mode - passing through ...");
return "";
}
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return null;
}
}
The other component is the AuthenticationProvider which will just check if the authentication contains a principal when it runs in prod mode (GWT prod) :
public class MyApplicationAuthenticationProvider implements AuthenticationProvider {
private static final Logger logger = Logger.getLogger(MyApplicationAuthenticationProvider.class);
public static final String SESSION_ID = "sessionId";
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (MyApplicationServerUtil.isProdMode()) {
if (StringUtils.isNotEmpty((String) authentication.getPrincipal())) {
logger.warn("Found credentials: " + (String) authentication.getPrincipal());
Authentication customAuth = new CustomAuthentication("ROLE_USER");
customAuth.setAuthenticated(true);
return customAuth;
} else {
throw new PreAuthenticatedCredentialsNotFoundException("Nothing returned from crosscontext for sessionId attribute ["
+ (String) authentication.getPrincipal() + "]");
}
} else {
Authentication customAuth = new CustomAuthentication("ROLE_USER");
customAuth.setAuthenticated(true);
return customAuth;
}
}
#Override
public boolean supports(Class<?> authentication) {
return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication);
}
}
I understand that it might not be the most secure application. However, it will already be running in a secure environment. But if you have suggestions for improvement, they're welcome !