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

Bugfix/43 Apple revoke use-case #205

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;

import java.util.List;

import org.apache.http.client.CookieStore;
import org.apache.http.cookie.Cookie;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -125,6 +127,67 @@ public void appleLoginTest() {
verifyLoggedInAndLogout(cookieFilter, "q_session_apple");
}

@Test
public void appleRevokeTest() {
// GIVEN a registered apple user
RenardeCookieFilter cookieFilter = new RenardeCookieFilter();
ValidatableResponse response = follow("/_renarde/security/login-apple", cookieFilter);
JsonPath json = response.statusCode(200)
.extract().body().jsonPath();
String code = json.get("code");
String state = json.get("state");

String location = given()
.when()
.filter(cookieFilter)
.formParam("state", state)
.formParam("code", code)
.redirects().follow(false)
.contentType("application/x-www-form-urlencoded")
.log().ifValidationFails()
.post("/_renarde/security/oidc-success")
.then()
.log().ifValidationFails()
.statusCode(302)
.extract().header("Location");
Assertions.assertNotNull(findCookie(cookieFilter.getCookieStore(), "q_session_apple"));
// add user (GET /oidc-success)
follow(location.replace("https://", "http://"), cookieFilter).statusCode(200);

// WHEN Revoke access
List<String> setCookieHeaderResp = given()
.when()
.filter(cookieFilter)
.redirects().follow(false)
.get("/_renarde/security/apple-revoke")
.then()
.statusCode(303)
.extract().headers()
.getValues("Set-Cookie");

// THEN Cookie is reset (no more QuarkusUser nor apple session)
String userLogoutCookie = setCookieHeaderResp.stream()
.filter(c -> c.startsWith("QuarkusUser="))
.findFirst()
.orElseThrow();
Assertions.assertEquals("QuarkusUser=;Version=1;Path=/;Max-Age=0", userLogoutCookie);
String userSessionCookie = setCookieHeaderResp.stream()
.filter(c -> c.startsWith("q_session_apple="))
.findFirst()
.orElseThrow();
Assertions.assertEquals("q_session_apple=;Version=1;Path=/;Max-Age=0", userSessionCookie);

// THEN Secure page will redirect to login
Assertions.assertTrue(
given().when().filter(cookieFilter)
.redirects().follow(false)
.get("/SecureController/hello")
.then()
.statusCode(302).extract().header("Location")
.endsWith("_renarde/security/login"));

}

private void verifyLoggedInAndLogout(RenardeCookieFilter cookieFilter, String cookieName) {
// can go to protected page
given()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ protected void registerRoutes(Router router) {
router.get("/auth/authorize").handler(this::authorize);
router.post("/auth/token").handler(bodyHandler).handler(this::accessTokenJson);
router.get("/auth/keys").handler(this::getKeys);
router.post("/auth/revoke").handler(this::revoke);

KeyPairGenerator kpg;
try {
Expand Down Expand Up @@ -73,6 +74,7 @@ protected void registerRoutes(Router router) {
public Map<String, String> start() {
Map<String, String> ret = super.start();
ret.put("quarkus.oidc.apple.credentials.jwt.key-file", "test.oidc-apple-key.pem");
ret.put("quarkus.rest-client.RenardeAppleClient.url", baseURI);
return ret;
}

Expand Down Expand Up @@ -245,4 +247,20 @@ private void getKeys(RoutingContext rc) {
.putHeader("Content-Type", "application/json")
.endAndForget(data);
}

/**
* POST /auth/revoke
* Host: appleid.apple.com
* Content-Type: application/x-www-form-urlencoded
*
* client_id=$1
* &client_secret=$2
* &token=$3
* &token_type_hint=access_token
*
* https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens/
*/
private void revoke(RoutingContext rc) {
rc.response().setStatusCode(200).endAndForget();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkiverse.renarde.oidc.impl;

import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@RegisterRestClient(baseUri = "https://appleid.apple.com")
public interface RenardeAppleClient {

@POST
@Path("/auth/revoke")
void revokeAppleUser(@FormParam("client_id") String clientID,
@FormParam("client_secret") String clientSecret,
@FormParam("token") String token,
@FormParam("token_type_hint") String tokenTypeHint);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.quarkiverse.renarde.oidc.impl;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;

import jakarta.inject.Inject;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import io.quarkiverse.renarde.Controller;
import io.quarkiverse.renarde.security.RenardeSecurity;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.security.Authenticated;
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
import io.smallrye.jwt.build.Jwt;
import io.vertx.ext.web.RoutingContext;

@Path("_renarde/security")
public class RenardeRevokeController extends Controller {

@Inject
AccessTokenCredential accessToken;

@RestClient
RenardeAppleClient renardeAppleClient;

@Inject
RoutingContext context;

@ConfigProperty(name = "quarkus.oidc.apple.client-id")
String appleClientId;

@ConfigProperty(name = "quarkus.oidc.apple.credentials.jwt.issuer")
String appleOidcIssuer;

@ConfigProperty(name = "quarkus.oidc.apple.credentials.jwt.token-key-id")
String appleOidcKeyId;

@ConfigProperty(name = "quarkus.oidc.apple.credentials.jwt.key-file", defaultValue = "AuthKey_XXX.p8")
String appleKeyFile;

@Inject
public RenardeSecurity security;

@Path("apple-revoke")
@Authenticated
public Response revokeApple() {
String tenant = context.get("tenant-id");
if ("apple".equalsIgnoreCase(tenant)) {
// Build secret using apple PK
String clientSecret = Jwt.audience("https://appleid.apple.com")
.subject(appleClientId)
.issuer(appleOidcIssuer)
.issuedAt(Instant.now().getEpochSecond())
.expiresIn(Duration.ofHours(1))
.jws()
.keyId(appleOidcKeyId)
.algorithm(SignatureAlgorithm.ES256)
.sign(getPrivateKey(appleKeyFile));
// Invalid refresh token
if (null != accessToken.getRefreshToken()) {
renardeAppleClient.revokeAppleUser(appleClientId, clientSecret, accessToken.getRefreshToken().getToken(),
"refresh_token");
}
// Invalid access token
renardeAppleClient.revokeAppleUser(appleClientId, clientSecret, accessToken.getToken(), "access_token");
}
// Invalid cookies
return security.makeLogoutResponse();
}

private PrivateKey getPrivateKey(String filename) {
try {
String content = Files.readString(Paths.get("target/classes/" + filename));
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
throw new RuntimeException(e);
}
}

}