Skip to content
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

OZ-573: Add support to authenticate with oauth2 via camel route #41

Merged
merged 2 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ $RECYCLE.BIN/

# Windows shortcuts
*.lnk
.java-version


# End of https://www.gitignore.io/api/git,java,maven,eclipse,windows
24 changes: 24 additions & 0 deletions commons/src/main/java/com/ozonehis/eip/security/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright © 2021, Ozone HIS <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.ozonehis.eip.security;

import lombok.NoArgsConstructor;

@NoArgsConstructor
public class Constants {

public static final String BEARER_HTTP_AUTH_SCHEME = "Bearer";

public static final String HEADER_OAUTH2_URL = "oauth2.url";

public static final String HEADER_OAUTH2_CLIENT_ID = "oauth2.client.id";

public static final String HEADER_OAUTH2_CLIENT_SECRET = "oauth2.client.secret";

public static final String HEADER_OAUTH2_SCOPE = "oauth2.client.scope";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright © 2021, Ozone HIS <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.ozonehis.eip.security.oauth2;

import static com.ozonehis.eip.security.Constants.BEARER_HTTP_AUTH_SCHEME;
import static com.ozonehis.eip.security.Constants.HEADER_OAUTH2_CLIENT_ID;
import static com.ozonehis.eip.security.Constants.HEADER_OAUTH2_CLIENT_SECRET;
import static com.ozonehis.eip.security.Constants.HEADER_OAUTH2_SCOPE;
import static com.ozonehis.eip.security.Constants.HEADER_OAUTH2_URL;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.ProducerTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Setter
@Getter
@Component("eip.oauthProcessor")
public class OAuth2Processor implements Processor {

private final ProducerTemplate producerTemplate;

@Autowired
public OAuth2Processor(ProducerTemplate producerTemplate) {
this.producerTemplate = producerTemplate;
}

@Override
public void process(Exchange exchange) {
OAuth2Properties properties = OAuth2Properties.builder()
.authUrl(exchange.getMessage().getHeader(HEADER_OAUTH2_URL, String.class))
.clientScope(exchange.getMessage().getHeader(HEADER_OAUTH2_SCOPE, String.class))
.clientId(exchange.getMessage().getHeader(HEADER_OAUTH2_CLIENT_ID, String.class))
.clientSecret(exchange.getMessage().getHeader(HEADER_OAUTH2_CLIENT_SECRET, String.class))
.build();
validateOAuth2Properties(properties);
OAuth2Token authToken = callAccessTokenUri(properties.getAuthUrl(), buildOAuth2RequestBody(properties));
if (authToken == null) {
throw new IllegalStateException("OAuth2 token is null");
} else {
log.info("OAuth2 token is successfully obtained. Expires in {} seconds", authToken.getExpiresIn());
}
setAuthorizationHeader(exchange, authToken.getAccessToken());
}

private void validateOAuth2Properties(OAuth2Properties properties) {
if (properties == null || !properties.isValid()) {
throw new IllegalStateException("OAuth2 properties are not set properly or some properties are missing");
}
}

private String buildOAuth2RequestBody(OAuth2Properties properties) {
return "grant_type=client_credentials" + "&client_id="
+ properties.getClientId() + "&client_secret="
+ properties.getClientSecret() + "&scope="
+ properties.getClientScope();
}

private OAuth2Token callAccessTokenUri(String accessTokenUri, String requestBody) {
return producerTemplate.requestBodyAndHeader(
"direct:oauth2", requestBody, HEADER_OAUTH2_URL, accessTokenUri, OAuth2Token.class);
}

private void setAuthorizationHeader(Exchange exchange, String accessToken) {
exchange.getMessage().setHeader("Authorization", BEARER_HTTP_AUTH_SCHEME + " " + accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright © 2021, Ozone HIS <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.ozonehis.eip.security.oauth2;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OAuth2Properties {

private String authUrl;

private String clientId;

private String clientSecret;

private String clientScope;

public boolean isValid() {
return authUrl != null && clientId != null && clientSecret != null && clientScope != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright © 2021, Ozone HIS <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.ozonehis.eip.security.oauth2;

import static com.ozonehis.eip.security.Constants.HEADER_OAUTH2_URL;

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.dataformat.JsonLibrary;
import org.springframework.stereotype.Component;

@Component
public class OAuth2Route extends RouteBuilder {

@Override
public void configure() {
from("direct:oauth2")
.routeId("oauth2")
.setHeader("Content-Type", constant("application/x-www-form-urlencoded"))
.setHeader("CamelHttpMethod", constant("POST"))
.toD("${header." + HEADER_OAUTH2_URL + "}")
.unmarshal()
.json(JsonLibrary.Jackson, OAuth2Token.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright © 2021, Ozone HIS <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.ozonehis.eip.security.oauth2;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@EqualsAndHashCode
@JsonIgnoreProperties(ignoreUnknown = true)
public class OAuth2Token {

@JsonProperty("access_token")
private String accessToken;

@JsonProperty("expires_in")
private long expiresIn;

@JsonProperty("refresh_expires_in")
private long refreshExpiresIn;

@JsonProperty("token_type")
private String tokenType;

@JsonProperty("not-before-policy")
private long notBeforePolicy;

@JsonProperty("scope")
private String scope;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright © 2021, Ozone HIS <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.ozonehis.eip.security.oauth2;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;

import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;

public class OAuth2ProcessorTest extends CamelTestSupport {

public static final String TEST_ACCESS_TOKEN = "testAccessToken";

public static final String KEYCLOAK_SERVER_URL_AUTH = "https://keycloak-server-url/auth";

@Mock
private ProducerTemplate producerTemplate;

@InjectMocks
private OAuth2Processor oauth2Processor;

private static AutoCloseable mocksCloser;

@Mock
private Exchange exchange;

@Mock
private Message message;

@BeforeEach
public void setUp() {
mocksCloser = openMocks(this);
when(message.getHeader("oauth2.url", String.class)).thenReturn(KEYCLOAK_SERVER_URL_AUTH);
when(message.getHeader("oauth2.client.id", String.class)).thenReturn("clientId");
when(message.getHeader("oauth2.client.secret", String.class)).thenReturn("clientSecret");
when(message.getHeader("oauth2.client.scope", String.class)).thenReturn("scope");
when(exchange.getMessage()).thenReturn(message);
}

@AfterAll
public static void closeMocks() throws Exception {
mocksCloser.close();
}

@Test
public void shouldAddAuthorizationHeaderGivenClientIdAndSecret() {
// Setup
when(producerTemplate.requestBodyAndHeader(
anyString(), anyString(), anyString(), anyString(), eq(OAuth2Token.class)))
.thenReturn(getOauthToken());

// Replay
oauth2Processor.process(exchange);

// Verify
verify(exchange.getMessage(), times(1)).setHeader(eq("Authorization"), eq("Bearer " + TEST_ACCESS_TOKEN));
}

@Test
public void shouldThrowEIPAuthenticationExceptionGivenNullClientScope() {
// Setup
String accessTokenUri = "https://test.auth.com/token";
when(producerTemplate.requestBodyAndHeader(
anyString(), anyString(), eq("authUrl"), eq(accessTokenUri), eq(OAuth2Token.class)))
.thenReturn(getOauthToken());

// Replay & Verify
assertThrows(IllegalStateException.class, () -> oauth2Processor.process(exchange));
}

@Test
public void shouldThrowEIPAuthenticationExceptionGivenNullProperties() {
// Setup
String accessTokenUri = "https://test.auth.com/token";
when(producerTemplate.requestBodyAndHeader(
anyString(), anyString(), eq("authUrl"), eq(accessTokenUri), eq(OAuth2Token.class)))
.thenReturn(getOauthToken());

// Replay & Verify
assertThrows(IllegalStateException.class, () -> oauth2Processor.process(exchange));
}

private static OAuth2Token getOauthToken() {
OAuth2Token oAuthToken = new OAuth2Token();
oAuthToken.setAccessToken(TEST_ACCESS_TOKEN);
oAuthToken.setExpiresIn(3600);
oAuthToken.setRefreshExpiresIn(3600);
oAuthToken.setTokenType("Bearer");
oAuthToken.setNotBeforePolicy(0);
return oAuthToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright © 2021, Ozone HIS <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.ozonehis.eip.security.oauth2;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.Test;

public class OAuth2PropertiesTest {

@Test
public void shouldSetPropertiesCorrectlyWithBuilder() {
// Set the properties using the builder
OAuth2Properties properties = OAuth2Properties.builder()
.authUrl("https://example.com/auth")
.clientId("testClientId")
.clientSecret("testClientSecret")
.clientScope("testClientScope")
.build();

// Verify
assertEquals("https://example.com/auth", properties.getAuthUrl());
assertEquals("testClientId", properties.getClientId());
assertEquals("testClientSecret", properties.getClientSecret());
assertEquals("testClientScope", properties.getClientScope());
}

@Test
public void shouldSetPropertiesCorrectlyWithDefaults() {
// set default properties
OAuth2Properties properties = OAuth2Properties.builder().build();

// Verify
assertNull(properties.getAuthUrl());
assertNull(properties.getClientId());
assertNull(properties.getClientSecret());
assertNull(properties.getClientScope());
}

@Test
public void shouldSetPropertiesCorrectlyWithSetter() {
OAuth2Properties properties = new OAuth2Properties();

// Set the properties using the setter methods
properties.setAuthUrl("https://example.com/auth");
properties.setClientId("testClientId");
properties.setClientSecret("testClientSecret");
properties.setClientScope("testClientScope");

// Verify
assertEquals("https://example.com/auth", properties.getAuthUrl());
assertEquals("testClientId", properties.getClientId());
assertEquals("testClientSecret", properties.getClientSecret());
assertEquals("testClientScope", properties.getClientScope());
}
}
Loading