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

OIDC UserInfo request must not be made if the token verification fails #34994

Merged
merged 1 commit into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ public class OidcIdentityProvider implements IdentityProvider<TokenAuthenticatio
static final String NEW_AUTHENTICATION = "new_authentication";

private static final Uni<TokenVerificationResult> NULL_CODE_ACCESS_TOKEN_UNI = Uni.createFrom().nullItem();
private static final Uni<UserInfo> NULL_USER_INFO_UNI = Uni.createFrom().nullItem();
private static final String CODE_ACCESS_TOKEN_RESULT = "code_flow_access_token_result";

@Inject
Expand Down Expand Up @@ -107,21 +106,21 @@ && isOpaqueAccessToken(vertxContext, request, resolvedContext)) {
// Typically it will be done for bearer access tokens therefore even if the access token has expired
// the client will be able to refresh if needed, no refresh token is available to Quarkus during the
// bearer access token verification

Uni<UserInfo> userInfo = resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false)
? getUserInfoUni(vertxContext, request, resolvedContext)
: NULL_USER_INFO_UNI;

return userInfo.onItemOrFailure().transformToUni(
new BiFunction<UserInfo, Throwable, Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
if (resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false)) {
return getUserInfoUni(vertxContext, request, resolvedContext).onItemOrFailure().transformToUni(
new BiFunction<UserInfo, Throwable, Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
return validateTokenWithUserInfoAndCreateIdentity(vertxContext, request, resolvedContext,
userInfo);
}
return validateTokenWithUserInfoAndCreateIdentity(vertxContext, request, resolvedContext, userInfo);
}
});
});
} else {
return validateTokenWithUserInfoAndCreateIdentity(vertxContext, request, resolvedContext, null);
}
} else {
final Uni<TokenVerificationResult> primaryTokenUni;
if (isInternalIdToken(request)) {
Expand Down Expand Up @@ -184,7 +183,20 @@ public Uni<SecurityIdentity> apply(TokenVerificationResult codeAccessToken, Thro
Uni<TokenVerificationResult> tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken(),
false, userInfo);

return createSecurityIdentityWithOidcServer(tokenUni, vertxContext, request, resolvedContext, userInfo);
return tokenUni.onItemOrFailure()
.transformToUni(
new BiFunction<TokenVerificationResult, Throwable, Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> apply(TokenVerificationResult result, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}

return createSecurityIdentityWithOidcServer(result, vertxContext, request,
resolvedContext, userInfo);
}
});

}
});
}
Expand All @@ -193,20 +205,32 @@ private Uni<SecurityIdentity> getUserInfoAndCreateIdentity(Uni<TokenVerification
RoutingContext vertxContext, TokenAuthenticationRequest request,
TenantConfigContext resolvedContext) {

Uni<UserInfo> userInfo = resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false)
? getUserInfoUni(vertxContext, request, resolvedContext)
: NULL_USER_INFO_UNI;

return userInfo.onItemOrFailure().transformToUni(
new BiFunction<UserInfo, Throwable, Uni<? extends SecurityIdentity>>() {
return tokenUni.onItemOrFailure()
.transformToUni(new BiFunction<TokenVerificationResult, Throwable, Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {
public Uni<SecurityIdentity> apply(TokenVerificationResult result, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
return createSecurityIdentityWithOidcServer(tokenUni, vertxContext, request, resolvedContext, userInfo);
if (resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false)) {
return getUserInfoUni(vertxContext, request, resolvedContext).onItemOrFailure().transformToUni(
new BiFunction<UserInfo, Throwable, Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
return createSecurityIdentityWithOidcServer(result, vertxContext, request,
resolvedContext, userInfo);
}
});
} else {
return createSecurityIdentityWithOidcServer(result, vertxContext, request, resolvedContext, null);
}

}
});

}

private boolean isOpaqueAccessToken(RoutingContext vertxContext, TokenAuthenticationRequest request,
Expand All @@ -222,108 +246,100 @@ private boolean isOpaqueAccessToken(RoutingContext vertxContext, TokenAuthentica
return false;
}

private Uni<SecurityIdentity> createSecurityIdentityWithOidcServer(Uni<TokenVerificationResult> tokenUni,
private Uni<SecurityIdentity> createSecurityIdentityWithOidcServer(TokenVerificationResult result,
RoutingContext vertxContext, TokenAuthenticationRequest request, TenantConfigContext resolvedContext,
final UserInfo userInfo) {

return tokenUni.onItemOrFailure()
.transformToUni(new BiFunction<TokenVerificationResult, Throwable, Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> apply(TokenVerificationResult result, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
// Token has been verified, as a JWT or an opaque token, possibly involving
// an introspection request.
final TokenCredential tokenCred = request.getToken();

JsonObject tokenJson = result.localVerificationResult;
if (tokenJson == null) {
// JSON token representation may be null not only if it is an opaque access token
// but also if it is JWT and no JWK with a matching kid is available, asynchronous
// JWK refresh has not finished yet, but the fallback introspection request has succeeded.
tokenJson = OidcUtils.decodeJwtContent(tokenCred.getToken());
}
if (tokenJson != null) {
try {
OidcUtils.validatePrimaryJwtTokenType(resolvedContext.oidcConfig.token, tokenJson);
JsonObject rolesJson = getRolesJson(vertxContext, resolvedContext, tokenCred, tokenJson,
userInfo);
SecurityIdentity securityIdentity = validateAndCreateIdentity(vertxContext, tokenCred,
resolvedContext, tokenJson, rolesJson, userInfo, result.introspectionResult);
// If the primary token is a bearer access token then there's no point of checking if
// it should be refreshed as RT is only available for the code flow tokens
if (isIdToken(request)
&& tokenAutoRefreshPrepared(result, vertxContext, resolvedContext.oidcConfig)) {
return Uni.createFrom().failure(new TokenAutoRefreshException(securityIdentity));
} else {
return Uni.createFrom().item(securityIdentity);
}
} catch (Throwable ex) {
return Uni.createFrom().failure(new AuthenticationFailedException(ex));
}
} else if (isIdToken(request)
|| tokenCred instanceof AccessTokenCredential
&& !((AccessTokenCredential) tokenCred).isOpaque()) {
return Uni.createFrom()
.failure(new AuthenticationFailedException("JWT token can not be converted to JSON"));
} else {
// ID Token or Bearer access token has been introspected or verified via Userinfo acquisition
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.addCredential(tokenCred);
OidcUtils.setSecurityIdentityUserInfo(builder, userInfo);
OidcUtils.setSecurityIdentityConfigMetadata(builder, resolvedContext);
final String userName;
if (result.introspectionResult == null) {
if (resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection &&
resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false)) {
if (resolvedContext.oidcConfig.token.principalClaim.isPresent() && userInfo != null) {
userName = userInfo.getString(resolvedContext.oidcConfig.token.principalClaim.get());
} else {
userName = "";
}
} else {
// we don't expect this to ever happen
LOG.debug("Illegal state - token introspection result is not available.");
return Uni.createFrom().failure(new AuthenticationFailedException());
}
} else {
OidcUtils.setSecurityIdentityIntrospection(builder, result.introspectionResult);
String principalName = result.introspectionResult.getUsername();
if (principalName == null) {
principalName = result.introspectionResult.getSubject();
}
userName = principalName != null ? principalName : "";
// Token has been verified, as a JWT or an opaque token, possibly involving
// an introspection request.
final TokenCredential tokenCred = request.getToken();

Set<String> scopes = result.introspectionResult.getScopes();
if (scopes != null) {
builder.addRoles(scopes);
}
}
builder.setPrincipal(new Principal() {
@Override
public String getName() {
return userName != null ? userName : "";
}
});
if (userInfo != null) {
OidcUtils.setSecurityIdentityRoles(builder, resolvedContext.oidcConfig,
new JsonObject(userInfo.getJsonObject().toString()));
}
OidcUtils.setBlockingApiAttribute(builder, vertxContext);
OidcUtils.setTenantIdAttribute(builder, resolvedContext.oidcConfig);
OidcUtils.setRoutingContextAttribute(builder, vertxContext);
SecurityIdentity identity = builder.build();
// If the primary token is a bearer access token then there's no point of checking if
// it should be refreshed as RT is only available for the code flow tokens
if (isIdToken(request)
&& tokenAutoRefreshPrepared(result, vertxContext, resolvedContext.oidcConfig)) {
return Uni.createFrom().failure(new TokenAutoRefreshException(identity));
}
return Uni.createFrom().item(identity);
}
JsonObject tokenJson = result.localVerificationResult;
if (tokenJson == null) {
// JSON token representation may be null not only if it is an opaque access token
// but also if it is JWT and no JWK with a matching kid is available, asynchronous
// JWK refresh has not finished yet, but the fallback introspection request has succeeded.
tokenJson = OidcUtils.decodeJwtContent(tokenCred.getToken());
}
if (tokenJson != null) {
try {
OidcUtils.validatePrimaryJwtTokenType(resolvedContext.oidcConfig.token, tokenJson);
JsonObject rolesJson = getRolesJson(vertxContext, resolvedContext, tokenCred, tokenJson,
userInfo);
SecurityIdentity securityIdentity = validateAndCreateIdentity(vertxContext, tokenCred,
resolvedContext, tokenJson, rolesJson, userInfo, result.introspectionResult);
// If the primary token is a bearer access token then there's no point of checking if
// it should be refreshed as RT is only available for the code flow tokens
if (isIdToken(request)
&& tokenAutoRefreshPrepared(result, vertxContext, resolvedContext.oidcConfig)) {
return Uni.createFrom().failure(new TokenAutoRefreshException(securityIdentity));
} else {
return Uni.createFrom().item(securityIdentity);
}
} catch (Throwable ex) {
return Uni.createFrom().failure(new AuthenticationFailedException(ex));
}
} else if (isIdToken(request)
|| tokenCred instanceof AccessTokenCredential
&& !((AccessTokenCredential) tokenCred).isOpaque()) {
return Uni.createFrom()
.failure(new AuthenticationFailedException("JWT token can not be converted to JSON"));
} else {
// ID Token or Bearer access token has been introspected or verified via Userinfo acquisition
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.addCredential(tokenCred);
OidcUtils.setSecurityIdentityUserInfo(builder, userInfo);
OidcUtils.setSecurityIdentityConfigMetadata(builder, resolvedContext);
final String userName;
if (result.introspectionResult == null) {
if (resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection &&
resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false)) {
if (resolvedContext.oidcConfig.token.principalClaim.isPresent() && userInfo != null) {
userName = userInfo.getString(resolvedContext.oidcConfig.token.principalClaim.get());
} else {
userName = "";
}
});
} else {
// we don't expect this to ever happen
LOG.debug("Illegal state - token introspection result is not available.");
return Uni.createFrom().failure(new AuthenticationFailedException());
}
} else {
OidcUtils.setSecurityIdentityIntrospection(builder, result.introspectionResult);
String principalName = result.introspectionResult.getUsername();
if (principalName == null) {
principalName = result.introspectionResult.getSubject();
}
userName = principalName != null ? principalName : "";

Set<String> scopes = result.introspectionResult.getScopes();
if (scopes != null) {
builder.addRoles(scopes);
}
}
builder.setPrincipal(new Principal() {
@Override
public String getName() {
return userName != null ? userName : "";
}
});
if (userInfo != null) {
OidcUtils.setSecurityIdentityRoles(builder, resolvedContext.oidcConfig,
new JsonObject(userInfo.getJsonObject().toString()));
}
OidcUtils.setBlockingApiAttribute(builder, vertxContext);
OidcUtils.setTenantIdAttribute(builder, resolvedContext.oidcConfig);
OidcUtils.setRoutingContextAttribute(builder, vertxContext);
SecurityIdentity identity = builder.build();
// If the primary token is a bearer access token then there's no point of checking if
// it should be refreshed as RT is only available for the code flow tokens
if (isIdToken(request)
&& tokenAutoRefreshPrepared(result, vertxContext, resolvedContext.oidcConfig)) {
return Uni.createFrom().failure(new TokenAutoRefreshException(identity));
}
return Uni.createFrom().item(identity);
}

}

private static boolean isInternalIdToken(TokenAuthenticationRequest request) {
Expand Down
Loading
Loading