Skip to content

Commit

Permalink
Merge pull request #34357 from Eng-Fouad/patch-1
Browse files Browse the repository at this point in the history
Add CIBA grant type to oidc-client
  • Loading branch information
sberyozkin authored Jul 2, 2023
2 parents f7d8303 + ec1152f commit 7cc2cd7
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -70,4 +73,13 @@ public Uni<String> echoRefreshTokenOnly(@QueryParam("refreshToken") String refre
public Uni<String> passwordGrantPublicClient() {
return clients.getClient("password-grant-public-client").getTokens().onItem().transform(t -> t.getAccessToken());
}

@GET
@Path("ciba-grant")
@Produces("text/plain")
public Uni<Response> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.it.keycloak;

public enum CibaAuthDeviceApprovalState {
PENDING,
APPROVED,
DENIED
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,45 @@ public Map<String, String> 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<String, String> conf = new HashMap<>();
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 7cc2cd7

Please sign in to comment.