Skip to content

Commit

Permalink
Merge pull request quarkusio#26917 from sberyozkin/oidc_introspection…
Browse files Browse the repository at this point in the history
…_authentication

Support authenticating to OpenID Introspection endpoint
  • Loading branch information
sberyozkin authored Jul 25, 2022
2 parents 1a3841b + 5ea4462 commit 022602f
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,18 @@ quarkus.oidc.tls.trust-store-password=${trust-store-password}
#quarkus.oidc.tls.trust-store-alias=certAlias
----

=== Introspection Endpoint Authentication

Some OpenID Connect Providers may require authenticating to its introspection endpoint using Basic Authentication with the credentials different to `client_id` and `client_secret` which may have already been configured to support `client_secret_basic` or `client_secret_post` client authentication methods described in the <<oidc-provider-client-authentication, Oidc Provider Client Authentication>> section.

If the tokens have to be introspected and the introspection endpoint specific authentication mechanism is required then you can configure `quarkus-oidc` like this:

[source,properties]
----
quarkus.oidc.introspection-credentials.name=introspection-user-name
quarkus.oidc.introspection-credentials.secret=introspection-user-secret
----

[[integration-testing]]
=== Testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,14 +373,17 @@ public static void verifyConfigurationId(String defaultId, String configKey, Opt

public static String initClientSecretBasicAuth(OidcCommonConfig oidcConfig) {
if (isClientSecretBasicAuthRequired(oidcConfig.credentials)) {
return OidcConstants.BASIC_SCHEME + " "
+ Base64.getEncoder().encodeToString(
(oidcConfig.getClientId().get() + ":"
+ clientSecret(oidcConfig.credentials)).getBytes(StandardCharsets.UTF_8));
return basicSchemeValue(oidcConfig.getClientId().get(), clientSecret(oidcConfig.credentials));
}
return null;
}

public static String basicSchemeValue(String name, String secret) {
return OidcConstants.BASIC_SCHEME + " "
+ Base64.getEncoder().encodeToString((name + ":" + secret).getBytes(StandardCharsets.UTF_8));

}

public static Key initClientJwtKey(OidcCommonConfig oidcConfig) {
if (isClientJwtAuthRequired(oidcConfig.credentials)) {
return clientJwtKey(oidcConfig.credentials);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,49 @@ public class OidcTenantConfig extends OidcCommonConfig {
@ConfigItem
public Optional<String> publicKey = Optional.empty();

/**
* Introspection Basic Authentication which must be configured only if the introspection is required
* and OpenId Connect Provider does not support the OIDC client authentication configured with
* {@link OidcCommonConfig#credentials} for its introspection endpoint.
*/
@ConfigItem
public IntrospectionCredentials introspectionCredentials = new IntrospectionCredentials();

/**
* Introspection Basic Authentication configuration
*/
@ConfigGroup
public static class IntrospectionCredentials {
/**
* Name
*/
@ConfigItem
public Optional<String> name = Optional.empty();

/**
* Secret
*/
@ConfigItem
public Optional<String> secret = Optional.empty();

public Optional<String> getName() {
return name;
}

public void setName(String name) {
this.name = Optional.of(name);
}

public Optional<String> getSecret() {
return secret;
}

public void setSecret(String secret) {
this.secret = Optional.of(secret);
}

}

/**
* Configuration to find and parse a custom claim containing the roles information.
*/
Expand Down Expand Up @@ -1203,4 +1246,12 @@ public boolean isCacheUserInfoInIdtoken() {
public void setCacheUserInfoInIdtoken(boolean cacheUserInfoInIdtoken) {
this.cacheUserInfoInIdtoken = cacheUserInfoInIdtoken;
}

public IntrospectionCredentials getIntrospectionCredentials() {
return introspectionCredentials;
}

public void setIntrospectionCredentials(IntrospectionCredentials introspectionCredentials) {
this.introspectionCredentials = introspectionCredentials;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class OidcProviderClient implements Closeable {
private final OidcConfigurationMetadata metadata;
private final OidcTenantConfig oidcConfig;
private final String clientSecretBasicAuthScheme;
private final String introspectionBasicAuthScheme;
private final Key clientJwtKey;

public OidcProviderClient(WebClient client,
Expand All @@ -50,6 +51,17 @@ public OidcProviderClient(WebClient client,
this.oidcConfig = oidcConfig;
this.clientSecretBasicAuthScheme = OidcCommonUtils.initClientSecretBasicAuth(oidcConfig);
this.clientJwtKey = OidcCommonUtils.initClientJwtKey(oidcConfig);
this.introspectionBasicAuthScheme = initIntrospectionBasicAuthScheme(oidcConfig);
}

private static String initIntrospectionBasicAuthScheme(OidcTenantConfig oidcConfig) {
if (oidcConfig.getIntrospectionCredentials().name.isPresent()
&& oidcConfig.getIntrospectionCredentials().secret.isPresent()) {
return OidcCommonUtils.basicSchemeValue(oidcConfig.getIntrospectionCredentials().name.get(),
oidcConfig.getIntrospectionCredentials().secret.get());
} else {
return null;
}
}

public OidcConfigurationMetadata getMetadata() {
Expand All @@ -72,7 +84,7 @@ public Uni<TokenIntrospection> introspectToken(String token) {
MultiMap introspectionParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
introspectionParams.add(OidcConstants.INTROSPECTION_TOKEN, token);
introspectionParams.add(OidcConstants.INTROSPECTION_TOKEN_TYPE_HINT, OidcConstants.ACCESS_TOKEN_VALUE);
return getHttpResponse(metadata.getIntrospectionUri(), introspectionParams)
return getHttpResponse(metadata.getIntrospectionUri(), introspectionParams, true)
.transform(resp -> getTokenIntrospection(resp));
}

Expand All @@ -96,21 +108,28 @@ public Uni<AuthorizationCodeTokens> getAuthorizationCodeTokens(String code, Stri
if (codeVerifier != null) {
codeGrantParams.add(OidcConstants.PKCE_CODE_VERIFIER, codeVerifier);
}
return getHttpResponse(metadata.getTokenUri(), codeGrantParams).transform(resp -> getAuthorizationCodeTokens(resp));
return getHttpResponse(metadata.getTokenUri(), codeGrantParams, false)
.transform(resp -> getAuthorizationCodeTokens(resp));
}

public Uni<AuthorizationCodeTokens> refreshAuthorizationCodeTokens(String refreshToken) {
MultiMap refreshGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
refreshGrantParams.add(OidcConstants.GRANT_TYPE, OidcConstants.REFRESH_TOKEN_GRANT);
refreshGrantParams.add(OidcConstants.REFRESH_TOKEN_VALUE, refreshToken);
return getHttpResponse(metadata.getTokenUri(), refreshGrantParams).transform(resp -> getAuthorizationCodeTokens(resp));
return getHttpResponse(metadata.getTokenUri(), refreshGrantParams, false)
.transform(resp -> getAuthorizationCodeTokens(resp));
}

private UniOnItem<HttpResponse<Buffer>> getHttpResponse(String uri, MultiMap formBody) {
private UniOnItem<HttpResponse<Buffer>> getHttpResponse(String uri, MultiMap formBody, boolean introspect) {
HttpRequest<Buffer> request = client.postAbs(uri);
request.putHeader(CONTENT_TYPE_HEADER, APPLICATION_X_WWW_FORM_URLENCODED);
request.putHeader(ACCEPT_HEADER, APPLICATION_JSON);
if (clientSecretBasicAuthScheme != null) {
if (introspect && introspectionBasicAuthScheme != null) {
request.putHeader(AUTHORIZATION_HEADER, introspectionBasicAuthScheme);
if (oidcConfig.clientId.isPresent()) {
formBody.set(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
}
} else if (clientSecretBasicAuthScheme != null) {
request.putHeader(AUTHORIZATION_HEADER, clientSecretBasicAuthScheme);
} else if (clientJwtKey != null) {
String jwt = OidcCommonUtils.signJwtWithKey(oidcConfig, metadata.getTokenUri(), clientJwtKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,21 @@ public OidcTenantConfig get() {
config.setAuthServerUrl(authServerUri);
config.setDiscoveryEnabled(false);
config.authentication.setUserInfoRequired(true);
config.setIntrospectionPath("introspect");
config.setUserInfoPath("userinfo");
if ("tenant-oidc-introspection-only".equals(tenantId)) {
config.setClientId("client-introspection-only");
config.setAllowTokenIntrospectionCache(false);
config.setAllowUserInfoCache(false);
Credentials creds = config.getCredentials();
creds.clientSecret.setMethod(Credentials.Secret.Method.POST_JWT);
creds.getJwt().setKeyFile("ecPrivateKey.pem");
creds.getJwt().setSignatureAlgorithm(SignatureAlgorithm.ES256.getAlgorithm());
} else {
config.setClientId("client-introspection-only-cache");
config.getIntrospectionCredentials().setName("bob");
config.getIntrospectionCredentials().setSecret("bob_secret");
}
config.setIntrospectionPath("introspect");
config.setUserInfoPath("userinfo");
config.setClientId("client-introspection-only");
Credentials creds = config.getCredentials();
creds.clientSecret.setMethod(Credentials.Secret.Method.POST_JWT);
creds.getJwt().setKeyFile("ecPrivateKey.pem");
creds.getJwt().setSignatureAlgorithm(SignatureAlgorithm.ES256.getAlgorithm());
return config;
} else if ("tenant-oidc-no-opaque-token".equals(tenantId)) {
OidcTenantConfig config = new OidcTenantConfig();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.quarkus.it.keycloak;

import java.security.PublicKey;
import java.util.Base64;

import javax.annotation.PostConstruct;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
Expand Down Expand Up @@ -106,26 +108,37 @@ public int resetIntrospectionEndpointCallCount() {
@POST
@Produces("application/json")
@Path("introspect")
public String introspect(@FormParam("client_secret") String secret) throws Exception {
public String introspect(@FormParam("client_id") String clientId, @FormParam("client_secret") String clientSecret,
@HeaderParam("Authorization") String authorization) throws Exception {
introspectionEndpointCallCount++;

String clientId = "undefined";
if (secret != null) {
String introspectionClientId = "none";
String introspectionClientSecret = "none";
if (clientSecret != null) {
// Secret is expected to be a JWT
PublicKey verificationKey = KeyUtils.readPublicKey("ecPublicKey.pem", SignatureAlgorithm.ES256);
JWTParser parser = new DefaultJWTParser();
// "client-introspection-only" is a client id, set as an issuer by default
JWTAuthContextInfo contextInfo = new JWTAuthContextInfo(verificationKey, "client-introspection-only");
contextInfo.setSignatureAlgorithm(SignatureAlgorithm.ES256);
JsonWebToken jwt = parser.parse(secret, contextInfo);
JsonWebToken jwt = parser.parse(clientSecret, contextInfo);
clientId = jwt.getIssuer();
} else if (authorization != null) {
String plainChallenge = new String(Base64.getDecoder().decode(authorization.substring("Basic ".length())));
int colonPos;
if ((colonPos = plainChallenge.indexOf(":")) > -1) {
introspectionClientId = plainChallenge.substring(0, colonPos);
introspectionClientSecret = plainChallenge.substring(colonPos + 1);
}
}

return "{" +
" \"active\": " + introspection + "," +
" \"scope\": \"user\"," +
" \"email\": \"[email protected]\"," +
" \"username\": \"alice\"," +
" \"introspection_client_id\": \"" + introspectionClientId + "\"," +
" \"introspection_client_secret\": \"" + introspectionClientSecret + "\"," +
" \"client_id\": \"" + clientId + "\"" +
" }";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public String userNameService(@PathParam("tenant") String tenant) {
if (tenant.startsWith("tenant-oidc-introspection-only")) {
TokenIntrospection introspection = securityIdentity.getAttribute("introspection");
response += (",client_id:" + introspection.getString("client_id"));
response += (",introspection_client_id:" + introspection.getString("introspection_client_id"));
response += (",introspection_client_secret:" + introspection.getString("introspection_client_secret"));
response += (",active:" + introspection.getBoolean("active"));
response += (",cache-size:" + tokenCache.getCacheSize());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,8 @@ public void testJwtTokenIntrospectionOnlyAndUserInfo() {
.then()
.statusCode(200)
.body(equalTo(
"tenant-oidc-introspection-only:alice,client_id:client-introspection-only,active:true,cache-size:0"));
"tenant-oidc-introspection-only:alice,client_id:client-introspection-only,"
+ "introspection_client_id:none,introspection_client_secret:none,active:true,cache-size:0"));
}

RestAssured.when().get("/oidc/jwk-endpoint-call-count").then().body(equalTo("0"));
Expand Down Expand Up @@ -441,7 +442,8 @@ private void verifyTokenIntrospectionAndUserInfoAreCached(String token1, int exp
.then()
.statusCode(200)
.body(equalTo(
"tenant-oidc-introspection-only-cache:alice,client_id:client-introspection-only,active:true,cache-size:"
"tenant-oidc-introspection-only-cache:alice,client_id:client-introspection-only-cache,"
+ "introspection_client_id:bob,introspection_client_secret:bob_secret,active:true,cache-size:"
+ expectedCacheSize));
}
RestAssured.when().get("/oidc/introspection-endpoint-call-count").then().body(equalTo("1"));
Expand Down

0 comments on commit 022602f

Please sign in to comment.