Skip to content

Commit

Permalink
Merge pull request #17896 from sberyozkin/oidc_introspection_improvem…
Browse files Browse the repository at this point in the history
…ents

Support the injection of OIDC introspection response
  • Loading branch information
sberyozkin authored Jun 16, 2021
2 parents efaaa15 + 6be08e8 commit cceeab7
Show file tree
Hide file tree
Showing 18 changed files with 349 additions and 87 deletions.
80 changes: 79 additions & 1 deletion docs/src/main/asciidoc/security-openid-connect.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,18 @@ Set `quarkus.oidc.user-info-required=true` if a UserInfo JSON object from the OI
A request will be sent to the OpenId Provider UserInfo endpoint and an `io.quarkus.oidc.UserInfo` (a simple `javax.json.JsonObject` wrapper) object will be created.
`io.quarkus.oidc.UserInfo` can be either injected or accessed as a SecurityIdentity `userinfo` attribute.

[[token-introspection]]
== Token Introspection

An opaque token has to be introspected by sending it to the OpenId Provider token introspection endpoint.

If the opaque token is active then a token introspection `username` and `scope` properties will be used to build a `Securityidentity`. Additionally, an `io.quarkus.oidc.TokenIntrospection` (a simple `javax.json.JsonObject` wrapper) object will be created and can be either injected or accessed as a SecurityIdentity `introspection` attribute.

Signed JWT tokens can also be introspected when no local matching `JsonWebKey` is available.

If you only work with JWT tokens then it is recommended to disable the opaque token introspection with `quarkus.oidc.token.allow-opaque-token-introspection=false`.
Additionally, disabling the introspection of signed JWT tokens is also advised with `quarkus.oidc.token.allow-jwt-introspection=false` if you expect that a local `JsonWebKey` will always be available since a 7`JsonWebKeySet` containing the public verification keys is periodically refreshed when the token has no matching `JsonWebKey`.

[[config-metadata]]
== Configuration Metadata

Expand Down Expand Up @@ -695,6 +707,7 @@ import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.Claim;
import io.quarkus.test.security.oidc.ConfigMetadata;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.quarkus.test.security.oidc.OidcConfigurationMetadata;
import io.quarkus.test.security.oidc.UserInfo;
import io.restassured.RestAssured;
Expand Down Expand Up @@ -730,7 +743,11 @@ where `ProtectedResource` class may look like this:

[source, java]
----
@Path("/web-app")
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.UserInfo;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/service")
@Authenticated
public class ProtectedResource {
Expand Down Expand Up @@ -762,6 +779,67 @@ Note that `@TestSecurity` annotation must always be used and its `user` property
`@OidcSecurity` annotation is optional and can be used to set the additional token claims, as well as `UserInfo` and `OidcConfigurationMetadata` properties.
Additionally, if `quarkus.oidc.token.issuer` property is configured then it will be used as an `OidcConfigurationMetadata` `issuer` property value.

If you work with the opaque tokens then you can test them as follows:

[source, java]
----
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.quarkus.test.security.oidc.TokenIntrospection;
import io.restassured.RestAssured;
@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {
@Test
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(introspectionRequired = true,
introspection = {
@TokenIntrospection(key = "email", value = "[email protected]")
}
)
public void testOidcWithClaimsUserInfoAndMetadata() {
RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then()
.body(is("userOidc:viewer:userOidc:viewer"));
}
}
----

where `ProtectedResource` class may look like this:

[source, java]
----
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/service")
@Authenticated
public class ProtectedResource {
@Inject
SecurityIdentity securityIdentity;
@Inject
TokenIntrospection introspection;
@GET
@Path("test-security-oidc-opaque-token")
public String testSecurityOidcOpaqueToken() {
return securityIdentity.getPrincipal().getName() + ":" + securityIdentity.getRoles().iterator().next()
+ ":" + introspection.getString("username")
+ ":" + introspection.getString("scope")
+ ":" + introspection.getString("email");
}
}
----

Note that `@TestSecurity` `user` and `roles` attributes are availabe as `TokenIntrospection` `username` and `scope` properties and you can use `io.quarkus.test.security.oidc.TokenIntrospection` to add the additional introspection response properties such as an `email`, etc.

== How to check the errors in the logs ==

Please enable `io.quarkus.oidc.runtime.OidcProvider` `TRACE` level logging to see more details about the token verification errors:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.oidc;

import javax.json.JsonObject;

import io.quarkus.oidc.runtime.AbstractJsonObjectResponse;

/**
* Represents a token introspection result
*
*/
public class TokenIntrospection extends AbstractJsonObjectResponse {

public TokenIntrospection() {
}

public TokenIntrospection(String introspectionJson) {
super(introspectionJson);
}

public TokenIntrospection(JsonObject json) {
super(json);
}
}
Original file line number Diff line number Diff line change
@@ -1,62 +1,19 @@
package io.quarkus.oidc;

import java.io.StringReader;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonValue;

public class UserInfo {
import io.quarkus.oidc.runtime.AbstractJsonObjectResponse;

private JsonObject json;
public class UserInfo extends AbstractJsonObjectResponse {

public UserInfo() {
}

public UserInfo(String userInfoJson) {
json = toJsonObject(userInfoJson);
super(userInfoJson);
}

public UserInfo(JsonObject json) {
this.json = json;
}

public String getString(String name) {
return json.getString(name);
}

public JsonArray getArray(String name) {
return json.getJsonArray(name);
}

public JsonObject getObject(String name) {
return json.getJsonObject(name);
}

public Object get(String name) {
return json.get(name);
}

public boolean contains(String propertyName) {
return json.containsKey(propertyName);
}

public Set<String> getPropertyNames() {
return Collections.unmodifiableSet(json.keySet());
}

public Set<Map.Entry<String, JsonValue>> getAllProperties() {
return Collections.unmodifiableSet(json.entrySet());
}

private static JsonObject toJsonObject(String userInfoJson) {
try (JsonReader jsonReader = Json.createReader(new StringReader(userInfoJson))) {
return jsonReader.readObject();
}
super(json);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.quarkus.oidc.runtime;

import java.io.StringReader;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonValue;

public class AbstractJsonObjectResponse {
private JsonObject json;

public AbstractJsonObjectResponse() {
}

public AbstractJsonObjectResponse(String introspectionJson) {
this(toJsonObject(introspectionJson));
}

public AbstractJsonObjectResponse(JsonObject json) {
this.json = json;
}

public String getString(String name) {
return json.getString(name);
}

public Boolean getBoolean(String name) {
return json.getBoolean(name);
}

public Long getLong(String name) {
JsonNumber number = json.getJsonNumber(name);
return number != null ? number.longValue() : null;
}

public JsonArray getArray(String name) {
return json.getJsonArray(name);
}

public JsonObject getObject(String name) {
return json.getJsonObject(name);
}

public Object get(String name) {
return json.get(name);
}

public boolean contains(String propertyName) {
return json.containsKey(propertyName);
}

public Set<String> getPropertyNames() {
return Collections.unmodifiableSet(json.keySet());
}

public Set<Map.Entry<String, JsonValue>> getAllProperties() {
return Collections.unmodifiableSet(json.entrySet());
}

private static JsonObject toJsonObject(String userInfoJson) {
try (JsonReader jsonReader = Json.createReader(new StringReader(userInfoJson))) {
return jsonReader.readObject();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,12 @@ public Uni<SecurityIdentity> apply(TokenVerificationResult result, Throwable t)
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.addCredential(tokenCred);
OidcUtils.setSecurityIdentityUserInfo(builder, userInfo);
OidcUtils.setSecurityIdentityIntrospecton(builder, result.introspectionResult);
OidcUtils.setSecurityIdentityConfigMetadata(builder, resolvedContext);
String principalMember = "";
if (result.introspectionResult.containsKey(OidcConstants.INTROSPECTION_TOKEN_USERNAME)) {
if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_USERNAME)) {
principalMember = OidcConstants.INTROSPECTION_TOKEN_USERNAME;
} else if (result.introspectionResult.containsKey(OidcConstants.INTROSPECTION_TOKEN_SUB)) {
} else if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_SUB)) {
// fallback to "sub", if "username" is not present
principalMember = OidcConstants.INTROSPECTION_TOKEN_SUB;
}
Expand All @@ -184,7 +185,7 @@ public String getName() {
return userName;
}
});
if (result.introspectionResult.containsKey(OidcConstants.TOKEN_SCOPE)) {
if (result.introspectionResult.contains(OidcConstants.TOKEN_SCOPE)) {
for (String role : result.introspectionResult.getString(OidcConstants.TOKEN_SCOPE).split(" ")) {
builder.addRole(role.trim());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
Expand Down Expand Up @@ -144,17 +145,17 @@ public Uni<? extends TokenVerificationResult> apply(Void v) {

public Uni<TokenVerificationResult> introspectToken(String token) {
return client.introspectToken(token).onItemOrFailure()
.transform(new BiFunction<JsonObject, Throwable, TokenVerificationResult>() {
.transform(new BiFunction<TokenIntrospection, Throwable, TokenVerificationResult>() {

@Override
public TokenVerificationResult apply(JsonObject jsonObject, Throwable t) {
public TokenVerificationResult apply(TokenIntrospection introspectionResult, Throwable t) {
if (t != null) {
throw new AuthenticationFailedException(t);
}
if (!Boolean.TRUE.equals(jsonObject.getBoolean(OidcConstants.INTROSPECTION_TOKEN_ACTIVE))) {
if (!Boolean.TRUE.equals(introspectionResult.getBoolean(OidcConstants.INTROSPECTION_TOKEN_ACTIVE))) {
throw new AuthenticationFailedException();
}
Long exp = jsonObject.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP);
Long exp = introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP);
if (exp != null) {
final int lifespanGrace = client.getOidcConfig().token.lifespanGrace.isPresent()
? client.getOidcConfig().token.lifespanGrace.getAsInt()
Expand All @@ -164,7 +165,7 @@ public TokenVerificationResult apply(JsonObject jsonObject, Throwable t) {
}
}

return new TokenVerificationResult(null, jsonObject);
return new TokenVerificationResult(null, introspectionResult);
}

});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.smallrye.mutiny.Uni;
Expand Down Expand Up @@ -58,7 +59,7 @@ public Uni<JsonObject> getUserInfo(String token) {
.send().onItem().transform(resp -> getUserInfo(resp));
}

public Uni<JsonObject> introspectToken(String token) {
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);
Expand Down Expand Up @@ -127,17 +128,29 @@ private JsonObject getUserInfo(HttpResponse<Buffer> resp) {
return getJsonObject(resp);
}

private JsonObject getTokenIntrospection(HttpResponse<Buffer> resp) {
return getJsonObject(resp);
private TokenIntrospection getTokenIntrospection(HttpResponse<Buffer> resp) {
return new TokenIntrospection(getString(resp));
}

private JsonObject getJsonObject(HttpResponse<Buffer> resp) {
private static JsonObject getJsonObject(HttpResponse<Buffer> resp) {
if (resp.statusCode() == 200) {
return resp.bodyAsJsonObject();
} else {
String errorMessage = resp.bodyAsString();
LOG.debugf("Request has failed: status: %d, error message: %s", resp.statusCode(), errorMessage);
throw new OIDCException(errorMessage);
throw responseException(resp);
}
}

private static String getString(HttpResponse<Buffer> resp) {
if (resp.statusCode() == 200) {
return resp.bodyAsString();
} else {
throw responseException(resp);
}
}

private static OIDCException responseException(HttpResponse<Buffer> resp) {
String errorMessage = resp.bodyAsString();
LOG.debugf("Request has failed: status: %d, error message: %s", resp.statusCode(), errorMessage);
throw new OIDCException(errorMessage);
}
}
Loading

0 comments on commit cceeab7

Please sign in to comment.