From ec1152f13287bae2b769f740814371ef09dd0332 Mon Sep 17 00:00:00 2001 From: Fouad Almalki Date: Sun, 2 Jul 2023 14:46:15 +0300 Subject: [PATCH] Add CIBA grant type to oidc-client --- ...urity-openid-connect-client-reference.adoc | 12 +++++ .../quarkus/oidc/client/OidcClientConfig.java | 7 ++- .../quarkus/it/keycloak/FrontendResource.java | 12 +++++ .../src/main/resources/application.properties | 6 +++ .../keycloak/CibaAuthDeviceApprovalState.java | 7 +++ .../quarkus/it/keycloak/InjectWireMock.java | 11 +++++ .../KeycloakRealmResourceManager.java | 45 +++++++++++++++++++ .../quarkus/it/keycloak/OidcClientTest.java | 39 ++++++++++++++++ 8 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/CibaAuthDeviceApprovalState.java create mode 100644 integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/InjectWireMock.java diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 7c3ed59087d5a..31eb3b8b11608 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -137,6 +137,18 @@ quarkus.oidc-client.grant.type=code and then you can use `OidcClient.accessTokens` method accepting a Map of extra properties and pass the current `code` and `redirect_uri` parameters to exchange the authorization code for the tokens. +`OidcClient` also supports the `urn:openid:params:grant-type:ciba` grant: + +[source,properties] +---- +quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ +quarkus.oidc-client.client-id=quarkus-app +quarkus.oidc-client.credentials.secret=secret +quarkus.oidc-client.grant.type=ciba +---- + +and then you can use `OidcClient.accessTokens` method accepting a Map of extra properties and pass `auth_req_id` parameter to exchange the authorization code for the tokens. + ==== Grant scopes You may need to request that a specific set of scopes is associated with an issued access token. diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java index d29b39c387123..52807d9f59e87 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java @@ -85,7 +85,12 @@ public static enum Type { * If 'quarkus.oidc-client.grant-type' is set to 'refresh' then `OidcClient` will only support refreshing the * tokens. */ - REFRESH("refresh_token"); + REFRESH("refresh_token"), + /** + * 'urn:openid:params:grant-type:ciba' grant requiring an OIDC client authentication as well as 'auth_req_id' + * parameter which must be passed to OidcClient at the token request time. + */ + CIBA("urn:openid:params:grant-type:ciba"); private String grantType; diff --git a/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index 2dee384ad1079..a558bed818041 100644 --- a/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -1,11 +1,14 @@ package io.quarkus.it.keycloak; +import java.util.Map; + import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.rest.client.inject.RestClient; @@ -70,4 +73,13 @@ public Uni echoRefreshTokenOnly(@QueryParam("refreshToken") String refre public Uni passwordGrantPublicClient() { return clients.getClient("password-grant-public-client").getTokens().onItem().transform(t -> t.getAccessToken()); } + + @GET + @Path("ciba-grant") + @Produces("text/plain") + public Uni cibaGrant(@QueryParam("authReqId") String authReqId) { + return clients.getClient("ciba-grant").getTokens(Map.of("auth_req_id", authReqId)) + .onItem().transform(t -> Response.ok(t.getAccessToken()).build()) + .onFailure(OidcClientException.class).recoverWithItem(t -> Response.status(400).entity(t.getMessage()).build()); + } } diff --git a/integration-tests/oidc-client-wiremock/src/main/resources/application.properties b/integration-tests/oidc-client-wiremock/src/main/resources/application.properties index d8dad093782a0..cba53b337eb5f 100644 --- a/integration-tests/oidc-client-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-client-wiremock/src/main/resources/application.properties @@ -44,6 +44,12 @@ quarkus.oidc-client.refresh.client-id=quarkus-app quarkus.oidc-client.refresh.credentials.secret=secret quarkus.oidc-client.refresh.grant.type=refresh +quarkus.oidc-client.ciba-grant.token-path=${keycloak.url}/ciba-token +quarkus.oidc-client.ciba-grant.client-id=quarkus-app +quarkus.oidc-client.ciba-grant.credentials.client-secret.value=secret +quarkus.oidc-client.ciba-grant.credentials.client-secret.method=POST +quarkus.oidc-client.ciba-grant.grant.type=ciba + io.quarkus.it.keycloak.ProtectedResourceServiceOidcClient/mp-rest/url=http://localhost:8081/protected quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/CibaAuthDeviceApprovalState.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/CibaAuthDeviceApprovalState.java new file mode 100644 index 0000000000000..9cd9d981ff804 --- /dev/null +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/CibaAuthDeviceApprovalState.java @@ -0,0 +1,7 @@ +package io.quarkus.it.keycloak; + +public enum CibaAuthDeviceApprovalState { + PENDING, + APPROVED, + DENIED +} diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/InjectWireMock.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/InjectWireMock.java new file mode 100644 index 0000000000000..22cb7b7648298 --- /dev/null +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/InjectWireMock.java @@ -0,0 +1,11 @@ +package io.quarkus.it.keycloak; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface InjectWireMock { +} diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index b89c36053549f..b118787f6322f 100644 --- a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -67,6 +67,45 @@ public Map start() { .withBody( "{\"access_token\":\"temp_access_token\", \"expires_in\":4}"))); + server.stubFor(WireMock.post("/ciba-token") + .withRequestBody(matching( + "grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba&client_id=quarkus-app&client_secret=secret&auth_req_id=16cdaa49-9591-4b63-b188-703fa3b25031")) + .willReturn(WireMock + .badRequest() + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody( + "{\"error\":\"expired_token\"}"))); + server.stubFor(WireMock.post("/ciba-token") + .withRequestBody(matching( + "grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba&client_id=quarkus-app&client_secret=secret&auth_req_id=b1493f2f-c25c-40f5-8d69-94e2ad4b06df")) + .inScenario("auth-device-approval") + .whenScenarioStateIs(CibaAuthDeviceApprovalState.PENDING.name()) + .willReturn(WireMock + .badRequest() + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody( + "{\"error\":\"authorization_pending\"}"))); + server.stubFor(WireMock.post("/ciba-token") + .withRequestBody(matching( + "grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba&client_id=quarkus-app&client_secret=secret&auth_req_id=b1493f2f-c25c-40f5-8d69-94e2ad4b06df")) + .inScenario("auth-device-approval") + .whenScenarioStateIs(CibaAuthDeviceApprovalState.DENIED.name()) + .willReturn(WireMock + .badRequest() + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody( + "{\"error\":\"access_denied\"}"))); + server.stubFor(WireMock.post("/ciba-token") + .withRequestBody(matching( + "grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba&client_id=quarkus-app&client_secret=secret&auth_req_id=b1493f2f-c25c-40f5-8d69-94e2ad4b06df")) + .inScenario("auth-device-approval") + .whenScenarioStateIs(CibaAuthDeviceApprovalState.APPROVED.name()) + .willReturn(WireMock + .ok() + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody( + "{\"access_token\":\"ciba_access_token\", \"expires_in\":4, \"refresh_token\":\"ciba_refresh_token\"}"))); + LOG.infof("Keycloak started in mock mode: %s", server.baseUrl()); Map conf = new HashMap<>(); @@ -82,4 +121,10 @@ public synchronized void stop() { server = null; } } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(server, + new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class, WireMockServer.class)); + } } diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index 17b490364fb0a..e98e42e2cd9b9 100644 --- a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -20,6 +20,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import com.github.tomakehurst.wiremock.WireMockServer; + import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @@ -28,6 +30,9 @@ @QuarkusTestResource(KeycloakRealmResourceManager.class) public class OidcClientTest { + @InjectWireMock + WireMockServer server; + @Test public void testEchoAndRefreshTokens() { // access_token_1 and refresh_token_1 are acquired using a password grant request. @@ -112,6 +117,40 @@ public void testEchoTokensRefreshTokenOnly() { .body(equalTo("temp_access_token")); } + @Test + public void testCibaGrant() { + RestAssured.given().queryParam("authReqId", "16cdaa49-9591-4b63-b188-703fa3b25031") + .when().get("/frontend/ciba-grant") + .then() + .statusCode(400) + .body(equalTo("{\"error\":\"expired_token\"}")); + + server.setScenarioState("auth-device-approval", CibaAuthDeviceApprovalState.PENDING.name()); + + RestAssured.given().queryParam("authReqId", "b1493f2f-c25c-40f5-8d69-94e2ad4b06df") + .when().get("/frontend/ciba-grant") + .then() + .statusCode(400) + .body(equalTo("{\"error\":\"authorization_pending\"}")); + + server.setScenarioState("auth-device-approval", CibaAuthDeviceApprovalState.DENIED.name()); + + RestAssured.given().queryParam("authReqId", "b1493f2f-c25c-40f5-8d69-94e2ad4b06df") + .when().get("/frontend/ciba-grant") + .then() + .statusCode(400) + .body(equalTo("{\"error\":\"access_denied\"}")); + + server.setScenarioState("auth-device-approval", CibaAuthDeviceApprovalState.APPROVED.name()); + + RestAssured.given().queryParam("authReqId", "b1493f2f-c25c-40f5-8d69-94e2ad4b06df") + .when().get("/frontend/ciba-grant") + .then() + .statusCode(200) + .body(equalTo("ciba_access_token")); + + } + private void checkLog() { final Path logDirectory = Paths.get(".", "target"); given().await().pollInterval(100, TimeUnit.MILLISECONDS)