Skip to content

MODULE: oauth2 #2755

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
8 changes: 7 additions & 1 deletion core/src/main/java/feign/BaseBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,13 @@ B enrich() {
}
});

return clone;
B enrichedBuilder = clone;

for (final Capability capability : capabilities) {
enrichedBuilder = capability.beforeBuild(enrichedBuilder);
}

return enrichedBuilder;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
Expand Down
14 changes: 14 additions & 0 deletions core/src/main/java/feign/Capability.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,18 @@ default <C> AsyncContextSupplier<C> enrich(AsyncContextSupplier<C> asyncContextS
default MethodInfoResolver enrich(MethodInfoResolver methodInfoResolver) {
return methodInfoResolver;
}

/**
* Hook executed before the build of Feign client is done. Any interceptors or retryers added by
* this method are not enriched by this or any other Capability.
*
* @param baseBuilder feign client builder
* @return enriched builder
* @param <B> builder class
* @param <T> target class
* @see OAuth2Authentication
*/
default <B extends BaseBuilder<B, T>, T> B beforeBuild(B baseBuilder) {
return baseBuilder;
}
}
53 changes: 37 additions & 16 deletions core/src/test/java/feign/BaseBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.RETURNS_MOCKS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.lang.reflect.Field;
import java.util.List;
Expand All @@ -29,16 +33,38 @@ class BaseBuilderTest {
@Test
void checkEnrichTouchesAllAsyncBuilderFields()
throws IllegalArgumentException, IllegalAccessException {
test(
AsyncFeign.builder()
.requestInterceptor(template -> {})
.responseInterceptor((ic, c) -> c.next(ic)),
14);
Capability mockingCapability =
test(
AsyncFeign.builder()
.requestInterceptor(template -> {})
.responseInterceptor((ic, c) -> c.next(ic)),
14);

// make sure capability was invoked only once
verify(mockingCapability).enrich(any(AsyncClient.class));
}

@Test
void checkEnrichTouchesAllBuilderFields()
throws IllegalArgumentException, IllegalAccessException {
Capability mockingCapability =
test(
Feign.builder()
.requestInterceptor(template -> {})
.responseInterceptor((ic, c) -> c.next(ic)),
12);

// make sure capability was invoked only once
verify(mockingCapability).enrich(any(Client.class));
}

private void test(BaseBuilder<?, ?> builder, int expectedFieldsCount)
private Capability test(BaseBuilder<?, ?> builder, int expectedFieldsCount)
throws IllegalArgumentException, IllegalAccessException {
Capability mockingCapability = Mockito.mock(Capability.class, RETURNS_MOCKS);
Capability mockingCapability = mock(Capability.class, RETURNS_MOCKS);
doAnswer(inv -> inv.getArgument(0, BaseBuilder.class))
.when(mockingCapability)
.beforeBuild(any(BaseBuilder.class));

BaseBuilder<?, ?> enriched = builder.addCapability(mockingCapability).enrich();

List<Field> fields = enriched.getFieldsToEnrich();
Expand All @@ -56,15 +82,10 @@ private void test(BaseBuilder<?, ?> builder, int expectedFieldsCount)
.isTrue();
assertNotSame(builder, enriched);
}
}

@Test
void checkEnrichTouchesAllBuilderFields()
throws IllegalArgumentException, IllegalAccessException {
test(
Feign.builder()
.requestInterceptor(template -> {})
.responseInterceptor((ic, c) -> c.next(ic)),
12);
// make sure capability was invoked only once
verify(mockingCapability).beforeBuild(any(BaseBuilder.class));

return mockingCapability;
}
}
88 changes: 88 additions & 0 deletions oauth2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Feign OAuth2

This module extends **Feign** to enable client authentication using
[OAuth2](https://datatracker.ietf.org/doc/html/rfc6749)
and
[OIDC](https://openid.net/specs/openid-connect-core-1_0.html)
frameworks.

It automatically authenticates the client against an **OAuth2/OpenID Connect (OIDC) Authorization Server** using
the `client_credentials` grant type.
Additionally, it manages **access token renewal** seamlessly.

### Supported Authentication Methods

- ✅ `client_secret_basic` (OAuth2)
- ✅ `client_secret_post` (OAuth2)
- ✅ `client_secret_jwt` (OIDC)
- ✅ `private_key_jwt` (OIDC)

### 🛠️ Upcoming Features (Planned Support)

- 🚀 `tls_client_auth` (RFC 8705)
- 🚀 `self_signed_tls_client_auth` (RFC 8705)

### Compatibility

Designed to work with most **OAuth2/OpenID Connect** providers.
Out-of-the-box support for:
- **AWS Cognito**
- **Okta Auth0**
- **Keycloak**

## Installation

### With Maven

```xml
<dependencies>
...
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-oauth2</artifactId>
</dependency>
...
</dependencies>
```

### With Gradle

```groovy
compile group: 'io.github.openfeign', name: 'feign-oauth2'
```

## Usage

Module provides `OAuth2Authentication` and `OpenIdAuthentication` generic capabilities, but also more specialized factory
classes: `AWSCognitoAuthentication`, `Auth0Authentication` and `KeycloakAuthentication`.

Here an example how to create an authenticated REST client by using **OIDC Discovery** of **Keycloak**:

```java
String issuer = String.format("http://keycloak:8080/realms/%s", "<keycloak realm>");

// Create an authentication
OpenIdAuthentication openIdAuthentication = OpenIdAuthentication.discover(ClientRegistration
.builder()
.credentials(Credentials
.builder()
.clientId("<client ID>")
.clientSecret("<client secret>")
.build())
.providerDetails(ProviderDetails
.builder()
.issuerUri(issuer)
.build())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.build());

IcecreamClient client = Feign
.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.addCapability(openIdAuthentication) // <-- add authentication to the Feign client
.target(IcecreamClient.class, "http://localhost:5555");

// This call to the service will be authenticated
Collection<Mixin> mixins = client.getAvailableMixins();
```
148 changes: 148 additions & 0 deletions oauth2/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright © 2012 The Feign Authors ([email protected])

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.github.openfeign</groupId>
<artifactId>parent</artifactId>
<version>13.6-SNAPSHOT</version>
</parent>

<artifactId>feign-oauth2</artifactId>

<name>Feign OAuth2</name>
<description>Provides support for the Client role as defined in the OAuth 2.0 Authorization Framework.</description>

<properties>
<java-jwt.version>4.5.0</java-jwt.version>

<spring-boot.version>3.4.0</spring-boot.version>
<inject-resources.version>0.3.5</inject-resources.version>
<testcontainers.version>1.20.4</testcontainers.version>
<testcontainers-keycloak.version>3.6.0</testcontainers-keycloak.version>
<dotenv-java.version>3.1.0</dotenv-java.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>

<!-- Feign -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>

<!-- OAuth2 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${java-jwt.version}</version>
</dependency>

<!-- Tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.hosuaby</groupId>
<artifactId>inject-resources-junit-jupiter</artifactId>
<version>${inject-resources.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>${testcontainers-keycloak.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>${dotenv-java.version}</version>
<scope>test</scope>
</dependency>

<!-- Mocking -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Loading