-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow to customize OIDC verification
- Loading branch information
1 parent
0929f70
commit a2679c8
Showing
19 changed files
with
371 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package io.quarkus.oidc; | ||
|
||
import static java.lang.annotation.ElementType.TYPE; | ||
|
||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* Qualifier which can be used to associate one or more OIDC features with a named tenant. | ||
*/ | ||
@Target({ TYPE }) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface Tenant { | ||
/** | ||
* Identifies an OIDC tenant to which a given feature applies. | ||
*/ | ||
String value(); | ||
} |
32 changes: 32 additions & 0 deletions
32
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenCustomizer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package io.quarkus.oidc; | ||
|
||
import jakarta.json.JsonObject; | ||
|
||
/** | ||
* TokenCustomizer can be used to change token headers to their original value for the token verification to succeed. | ||
* | ||
* Use it only if OIDC provider has changed some of the header values after the token signature has been created | ||
* for security reasons. Changing the headers in all other cases will lead to the token signature verification failure. | ||
* | ||
* Please note that JSON canonicalization is not performed as part of JWT token signing process. | ||
* It means that if OIDC provider adds ignorable characters such as spaces or newline characters to JSON | ||
* which represents token headers then these characters will also be included as an additional input to | ||
* the token signing process. In this case recreating exactly the same JSON token headers sequence after the headers | ||
* have been modified by this customizer will not be possible and the signature verification will fail. | ||
* | ||
* Custom token customizers should be registered and discoverable as CDI beans. | ||
* They should be bound to specific OIDC tenants with a {@link Tenant} qualifier. | ||
* with the exception of named customizers provided by this extension which have to be selected with | ||
* a `quarkus.oidc.token.customizer-name` property. | ||
* | ||
* Custom token customizers without a {@link Tenant} qualifier will be bound to all OIDC tenants. | ||
*/ | ||
public interface TokenCustomizer { | ||
/** | ||
* Customize token headers | ||
* | ||
* @param headers the token headers | ||
* @return modified headers, null can be returned to indicate no modification has taken place | ||
*/ | ||
JsonObject customizeHeaders(JsonObject headers); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenCustomizerFinder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package io.quarkus.oidc.runtime; | ||
|
||
import io.quarkus.arc.Arc; | ||
import io.quarkus.arc.ArcContainer; | ||
import io.quarkus.arc.InstanceHandle; | ||
import io.quarkus.oidc.OIDCException; | ||
import io.quarkus.oidc.OidcTenantConfig; | ||
import io.quarkus.oidc.Tenant; | ||
import io.quarkus.oidc.TokenCustomizer; | ||
|
||
public class TokenCustomizerFinder { | ||
|
||
public static TokenCustomizer find(OidcTenantConfig oidcConfig) { | ||
if (oidcConfig == null) { | ||
return null; | ||
} | ||
ArcContainer container = Arc.container(); | ||
if (container != null) { | ||
String customizerName = oidcConfig.token.customizerName.orElse(null); | ||
if (customizerName != null && !customizerName.isEmpty()) { | ||
InstanceHandle<TokenCustomizer> tokenCustomizer = container.instance(customizerName); | ||
if (tokenCustomizer.isAvailable()) { | ||
return tokenCustomizer.get(); | ||
} else { | ||
throw new OIDCException("Unable to find TokenCustomizer " + customizerName); | ||
} | ||
} else { | ||
for (InstanceHandle<TokenCustomizer> tokenCustomizer : container.listAll(TokenCustomizer.class)) { | ||
Tenant tenantAnn = tokenCustomizer.get().getClass().getAnnotation(Tenant.class); | ||
if (tenantAnn != null && oidcConfig.tenantId.get().equals(tenantAnn.value())) { | ||
return tokenCustomizer.get(); | ||
} | ||
} | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
} |
36 changes: 36 additions & 0 deletions
36
...c/runtime/src/main/java/io/quarkus/oidc/runtime/providers/AzureAccessTokenCustomizer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package io.quarkus.oidc.runtime.providers; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
|
||
import jakarta.enterprise.context.ApplicationScoped; | ||
import jakarta.inject.Named; | ||
import jakarta.json.Json; | ||
import jakarta.json.JsonObject; | ||
|
||
import io.quarkus.oidc.OIDCException; | ||
import io.quarkus.oidc.TokenCustomizer; | ||
import io.quarkus.oidc.runtime.OidcUtils; | ||
|
||
@Named("azure-access-token-customizer") | ||
@ApplicationScoped | ||
public class AzureAccessTokenCustomizer implements TokenCustomizer { | ||
private static final String NONCE = "nonce"; | ||
|
||
@Override | ||
public JsonObject customizeHeaders(JsonObject headers) { | ||
try { | ||
String nonce = headers.getString(NONCE); | ||
if (nonce != null) { | ||
byte[] nonceSha256 = OidcUtils.getSha256Digest(nonce.getBytes(StandardCharsets.UTF_8)); | ||
byte[] newNonceBytes = Base64.getUrlEncoder().withoutPadding().encode(nonceSha256); | ||
return Json.createObjectBuilder(headers) | ||
.add(NONCE, new String(newNonceBytes, StandardCharsets.UTF_8)).build(); | ||
} | ||
return null; | ||
} catch (Exception ex) { | ||
throw new OIDCException(ex); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcProviderTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package io.quarkus.oidc.runtime; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.fail; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
|
||
import jakarta.json.Json; | ||
import jakarta.json.JsonObject; | ||
|
||
import org.jose4j.jwk.RsaJsonWebKey; | ||
import org.jose4j.jwk.RsaJwkGenerator; | ||
import org.jose4j.jwt.consumer.InvalidJwtException; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import io.quarkus.oidc.OidcTenantConfig; | ||
import io.quarkus.oidc.TokenCustomizer; | ||
import io.smallrye.jwt.build.Jwt; | ||
|
||
public class OidcProviderTest { | ||
|
||
@SuppressWarnings("resource") | ||
@Test | ||
public void testAlgorithmCustomizer() throws Exception { | ||
|
||
RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); | ||
rsaJsonWebKey.setKeyId("k1"); | ||
|
||
final String token = Jwt.issuer("http://keycloak/ream").jws().keyId("k1").sign(rsaJsonWebKey.getPrivateKey()); | ||
final String newToken = replaceAlgorithm(token, "ES256"); | ||
JsonWebKeySet jwkSet = new JsonWebKeySet("{\"keys\": [" + rsaJsonWebKey.toJson() + "]}"); | ||
OidcTenantConfig oidcConfig = new OidcTenantConfig(); | ||
|
||
OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null); | ||
try { | ||
provider.verifyJwtToken(newToken, false); | ||
fail("InvalidJwtException expected"); | ||
} catch (InvalidJwtException ex) { | ||
// continue | ||
} | ||
|
||
provider = new OidcProvider(null, oidcConfig, jwkSet, new TokenCustomizer() { | ||
|
||
@Override | ||
public JsonObject customizeHeaders(JsonObject headers) { | ||
return Json.createObjectBuilder(headers).add("alg", "RS256").build(); | ||
} | ||
|
||
}, null); | ||
TokenVerificationResult result = provider.verifyJwtToken(newToken, false); | ||
assertEquals("http://keycloak/ream", result.localVerificationResult.getString("iss")); | ||
} | ||
|
||
private static String replaceAlgorithm(String token, String algorithm) { | ||
io.vertx.core.json.JsonObject headers = OidcUtils.decodeJwtHeaders(token); | ||
headers.put("alg", algorithm); | ||
String newHeaders = new String( | ||
Base64.getUrlEncoder().withoutPadding().encode(headers.toString().getBytes()), | ||
StandardCharsets.UTF_8); | ||
int dotIndex = token.indexOf('.'); | ||
return newHeaders + token.substring(dotIndex); | ||
} | ||
|
||
} |
Oops, something went wrong.