Skip to content
Draft
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
2 changes: 1 addition & 1 deletion vertx-auth-common/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@
exports io.vertx.ext.auth.impl.cose to io.vertx.auth.webauthn, io.vertx.auth.webauthn4j, io.vertx.tests;
exports io.vertx.ext.auth.impl.asn to io.vertx.auth.webauthn, io.vertx.auth.webauthn4j;
exports io.vertx.ext.auth.authorization.impl to io.vertx.auth.abac;
exports io.vertx.ext.auth.impl.http to io.vertx.auth.oauth2, io.vertx.auth.webauthn, io.vertx.auth.webauthn4j;
exports io.vertx.ext.auth.impl.http to io.vertx.auth.oauth2, io.vertx.auth.webauthn, io.vertx.auth.webauthn4j, io.vertx.tests;

}
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ public String getClientAssertionType() {

public OAuth2Options setClientAssertionType(String clientAssertionType) {
this.clientAssertionType = clientAssertionType;
this.useBasicAuthorization = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a shortcut that might introduce unexpected behaviors for some users

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry but I'm not an expert in that area. Can you elaborate about why this might come unexpected for some users? Especially as it seems you say the current behavior is broken in the other comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 2 ways how to use assertion. Both defined: https://datatracker.ietf.org/doc/html/rfc7521

This PR fixes incorrect implementation for Using Assertions for Client Authentication

As test were missing that would catch this, it was overlooked. This has been fixed by the PR and I am quite familiar with this.
On the other hand most of the code that is present its goal was to facilitate Using Assertions as Authorization Grants which I have limited knowledge. And looking at the test non of them validate the functionality so I am not 100% sure that something was not broken by this PR.

return this;
}

Expand Down Expand Up @@ -623,29 +624,19 @@ public void validate() throws IllegalStateException {
case AUTH_CODE:
case AUTH_JWT:
case AAD_OBO:
if (clientAssertion == null && clientAssertionType == null) {
if (clientAssertionType == null) {
// not using client assertions
if (clientId == null) {
throw new IllegalStateException("Configuration missing. You need to specify [clientId]");
}
} else {
if (clientAssertion == null || clientAssertionType == null) {
throw new IllegalStateException(
"Configuration missing. You need to specify [clientAssertion] AND [clientAssertionType]");
}
}
break;
case PASSWORD:
if (clientAssertion == null && clientAssertionType == null) {
if (clientAssertionType == null) {
// not using client assertions
if (clientId == null) {
LOG.debug("If you are using Client Oauth2 Resource Owner flow. You need to specify [clientId]");
}
} else {
if (clientAssertion == null || clientAssertionType == null) {
throw new IllegalStateException(
"Configuration missing. You need to specify [clientAssertion] AND [clientAssertionType]");
}
}
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,7 @@ public Future<JsonObject> token(String grantType, JsonObject params) {

form.put("grant_type", grantType);

if (!clientAuthentication(headers, form)) {
String clientId = config.getClientId();
if (clientId == null) {
if (config.getClientAssertionType() != null) {
form
.put("client_assertion_type", config.getClientAssertionType());
}
if (config.getClientAssertion() != null) {
form
.put("client_assertion", config.getClientAssertion());
}
}
}
clientAuthentication(headers, form);

headers.put("Content-Type", "application/x-www-form-urlencoded");
final Buffer payload = SimpleHttpClient.jsonToQuery(form);
Expand Down Expand Up @@ -414,15 +402,28 @@ public Future<JsonObject> userInfo(String accessToken, JWT jwt) {
}

private boolean clientAuthentication(JsonObject headers, JsonObject form) {
final boolean confidentialClient = config.getClientId() != null && config.getClientSecret() != null;
final boolean confidentialClient = config.getClientId() != null &&
(config.getClientSecret() != null || config.getClientAssertionType() != null);

if (confidentialClient) {
if (config.isUseBasicAuthorization()) {
String basic = config.getClientId() + ":" + config.getClientSecret();
headers.put("Authorization", "Basic " + base64Encode(basic.getBytes(StandardCharsets.UTF_8)));
} else {
form.put("client_id", config.getClientId());
form.put("client_secret", config.getClientSecret());

if (config.getClientAssertionType() != null) {
form.put("client_assertion_type", config.getClientAssertionType());
if (form.getString("client_assertion") == null) {
if (config.getClientAssertion() != null) {
form.put("client_assertion", config.getClientAssertion());
} else {
throw new RuntimeException(String.format("Can not authenticate client, client_assertion_type is set to %s but client_assertion is not configured", config.getClientAssertionType()));
}
}
} else {
form.put("client_secret", config.getClientSecret());
}
}
} else {
if (config.getClientId() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ public Future<User> authenticate(Credentials credentials) {
if (oauth2Credentials.getCodeVerifier() != null) {
params.put("code_verifier", oauth2Credentials.getCodeVerifier());
}
if (oauth2Credentials.getAssertion() != null) {
params.put("client_assertion", oauth2Credentials.getAssertion());
}
break;

case PASSWORD:
Expand Down
2 changes: 2 additions & 0 deletions vertx-auth-oauth2/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@
requires static io.vertx.codegen.json;
requires static io.vertx.docgen;

exports io.vertx.ext.auth.oauth2.impl to io.vertx.tests;

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ public class OAuth2AuthCodeTest {
.put("redirect_uri", "http://callback.com")
.put("grant_type", "authorization_code");

private static final Credentials tokenConfigSecretJwt = new Oauth2Credentials()
.setFlow(OAuth2FlowType.AUTH_CODE)
.setCode("code")
.setRedirectUri("http://callback.com")
.setAssertion("eyJhb");

private static final JsonObject oauthConfigSecretJwt = new JsonObject()
.put("code", "code")
.put("redirect_uri", "http://callback.com")
.put("client_assertion", "eyJhb")
.put("grant_type", "authorization_code")
.put("client_id", "client-id")
.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");

private static final OAuth2AuthorizationURL authorizeConfig = new OAuth2AuthorizationURL()
.setRedirectUri("http://localhost:3000/callback")
.addScope("user")
Expand All @@ -75,15 +89,26 @@ public void setUp(TestContext should) throws Exception {
.connectionHandler(c -> connectionCounter++)
.requestHandler(req -> {
if (req.method() == HttpMethod.POST && "/oauth/token".equals(req.path())) {
should.assertEquals("Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ=", req.getHeader("Authorization"));
req.setExpectMultipart(true).bodyHandler(buffer -> {
try {
should.assertEquals(config, SimpleHttpClient.queryToJson(buffer));
req.response().putHeader("Content-Type", "application/json").end(fixtureTokens.encode());
} catch (UnsupportedEncodingException e) {
should.fail(e);
}
});
if (req.getHeader("Authorization") != null) {
should.assertEquals("Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ=", req.getHeader("Authorization"));
req.setExpectMultipart(true).bodyHandler(buffer -> {
try {
should.assertEquals(config, SimpleHttpClient.queryToJson(buffer));
req.response().putHeader("Content-Type", "application/json").end(fixtureTokens.encode());
} catch (UnsupportedEncodingException e) {
should.fail(e);
}
});
} else {
req.setExpectMultipart(true).bodyHandler(buffer -> {
try {
should.assertEquals(config, SimpleHttpClient.queryToJson(buffer));
req.response().putHeader("Content-Type", "application/json").end(fixtureTokens.encode());
} catch (UnsupportedEncodingException e) {
should.fail(e);
}
});
}
} else if (req.method() == HttpMethod.GET && "/oauth/jwks".equals(req.path())) {
req.bodyHandler(buffer -> {
req.response().putHeader("Content-Type", "application/json").end(fixtureJwks.encode());
Expand Down Expand Up @@ -152,6 +177,25 @@ public void getToken(TestContext should) {
});
}

@Test
public void getTokenWithClientSecretJwt(TestContext should) {
final Async test = should.async();

config = oauthConfigSecretJwt;
oauth2 = OAuth2Auth.create(rule.vertx(), new OAuth2Options()
.setClientId(oauthConfigSecretJwt.getString("client_id"))
.setClientAssertionType(oauthConfigSecretJwt.getString("client_assertion_type"))
.setSite("http://localhost:" + currentPort));
oauth2.authenticate(tokenConfigSecretJwt)
.onFailure(should::fail)
.onSuccess(token -> {
should.assertNotNull(token);
should.assertNotNull(token.principal());
should.assertNotNull(token.principal().getString("access_token"));
test.complete();
});
}

@Test
public void testConnectionReuse(TestContext should) {
final Async test = should.async();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.authentication.TokenCredentials;
import io.vertx.ext.auth.authorization.RoleBasedAuthorization;
import io.vertx.ext.auth.impl.jose.JWT;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.auth.jwt.authorization.MicroProfileAuthorization;
import io.vertx.ext.auth.oauth2.OAuth2Auth;
import io.vertx.ext.auth.oauth2.OAuth2FlowType;
Expand All @@ -27,9 +32,12 @@
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

@RunWith(Parameterized.class)
@Parameterized.UseParametersRunnerFactory(VertxUnitRunnerWithParametersFactory.class)
Expand Down Expand Up @@ -453,6 +461,47 @@ public void discoverGetTokenFromFrontEndPerformAuthWithBorkendWillFail(TestConte
});
}

@Test
public void hardTest(TestContext should) {
final Async test = should.async();

OAuth2Options options = new OAuth2Options()
.setClientId("confidential-client-authenticator-signed-jwt")
.setTenant("vertx-it")
.setClientAssertionType("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
.setSite(site + "/auth/realms/{tenant}");

options.getHttpClientOptions().setTrustAll(true);

KeycloakAuth.discover(rule.vertx(), options)
.onFailure(should::fail)
.onSuccess(oauth2 -> {

JWTAuth provider = JWTAuth.create(rule.vertx(), new JWTAuthOptions()
.addPubSecKey(new PubSecKeyOptions()
.setAlgorithm("HS256")
.setBuffer("4120c155-7cd0-4c62-9dff-cfd36a1244f6"))
.setJWTOptions(new JWTOptions()
.addAudience(String.format("%s/auth/realms/%s/protocol/openid-connect/token", site, "vertx-it"))
.setSubject("confidential-client-authenticator-signed-jwt")
.setIssuer("confidential-client-authenticator-signed-jwt")
.setExpiresInSeconds(60)));
String token = provider.generateToken(new JsonObject().put("jti", UUID.randomUUID().toString()));

oauth2.authenticate(new Oauth2Credentials()
.setFlow(OAuth2FlowType.AUTH_CODE)
.setCode("testCode")
.setAssertion(token))
.onFailure(exception -> {
//this is a hacky way to check if authentication was successful without performing complicated code flow inside test
//if we get invalid code exception, it means that authentication was successful but code is invalid as expected
should.assertEquals("invalid_grant: Code not valid", exception.getMessage());
test.complete();
})
.onSuccess(result -> should.fail("This test should not succeed as we sent invalid code")) ;
});
}

private Future<User> loginAs(OAuth2Auth oauth2, TestContext should, String username, String audience, List<String> scopes) {
final Promise<User> promise = Promise.promise();

Expand Down
17 changes: 17 additions & 0 deletions vertx-auth-oauth2/src/test/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/
open module io.vertx.tests {
requires io.vertx.auth.oauth2;
requires io.vertx.auth.jwt;
requires io.vertx.auth.common;
requires io.vertx.testing.unit;
requires junit;
}
Loading
Loading