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

Verify code flow access token first if no UserInfo precondition exists #30485

Merged
merged 1 commit into from
Jan 20, 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 @@ -24,6 +24,7 @@ public final class OidcConstants {
public static final String INTROSPECTION_TOKEN = "token";
public static final String INTROSPECTION_TOKEN_ACTIVE = "active";
public static final String INTROSPECTION_TOKEN_EXP = "exp";
public static final String INTROSPECTION_TOKEN_IAT = "iat";
public static final String INTROSPECTION_TOKEN_USERNAME = "username";
public static final String INTROSPECTION_TOKEN_SUB = "sub";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1147,9 +1147,9 @@ public static Token fromAudience(String... audience) {
public Optional<String> principalClaim = Optional.empty();

/**
* Refresh expired ID tokens.
* If this property is enabled then a refresh token request will be performed if the ID token has expired
* and, if successful, the local session will be updated with the new set of tokens.
* Refresh expired authorization code flow ID or access tokens.
* If this property is enabled then a refresh token request will be performed if the authorization code
* ID or access token has expired and, if successful, the local session will be updated with the new set of tokens.
* Otherwise, the local session will be invalidated and the user redirected to the OpenID Provider to re-authenticate.
* In this case the user may not be challenged again if the OIDC provider session is still active.
*
Expand All @@ -1164,8 +1164,9 @@ public static Token fromAudience(String... audience) {
/**
* Refresh token time skew in seconds.
* If this property is enabled then the configured number of seconds is added to the current time
* when checking whether the access token should be refreshed. If the sum is greater than this access token's
* expiration time then a refresh is going to happen.
* when checking if the authorization code ID or access token should be refreshed.
* If the sum is greater than the authorization code ID or access token's expiration time then a refresh is going to
* happen.
*
* This property will be ignored if the 'refresh-expired' property is not enabled.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ public Uni<? extends SecurityIdentity> apply(Throwable t) {
return Uni.createFrom()
.failure(new AuthenticationCompletionException(t.getCause()));
}
// Token has expired, try to refresh
if (session.getRefreshToken() == null) {
LOG.debug(
"Token has expired, token refresh is not possible because the refresh token is null");
Expand All @@ -308,6 +309,7 @@ public Uni<? extends SecurityIdentity> apply(Throwable t) {
context,
identityProviderManager, false, null);
} else if (session.getRefreshToken() != null) {
// Token has nearly expired, try to refresh
LOG.debug("Token auto-refresh is starting");
return refreshSecurityIdentity(configContext,
currentIdToken,
Expand All @@ -317,9 +319,16 @@ public Uni<? extends SecurityIdentity> apply(Throwable t) {
((TokenAutoRefreshException) t).getSecurityIdentity());
} else {
LOG.debug(
"Token auto-refresh is required it is not possible because the refresh token is null");
"Token auto-refresh is required but is not possible because the refresh token is null");
// Auto-refreshing is not possible, just continue with the current security identity
return Uni.createFrom().item(((TokenAutoRefreshException) t).getSecurityIdentity());
SecurityIdentity currentIdentity = ((TokenAutoRefreshException) t)
.getSecurityIdentity();
if (currentIdentity != null) {
return Uni.createFrom().item(currentIdentity);
} else {
return Uni.createFrom()
.failure(new AuthenticationFailedException(t.getCause()));
}
}
}
});
Expand Down Expand Up @@ -971,9 +980,9 @@ private Uni<SecurityIdentity> refreshSecurityIdentity(TenantConfigContext config
public Uni<SecurityIdentity> apply(final AuthorizationCodeTokens tokens, final Throwable t) {
if (t != null) {
LOG.debugf("ID token refresh has failed: %s", t.getMessage());
if (autoRefresh) {
if (autoRefresh && fallback != null) {
LOG.debug("Using the current SecurityIdentity since the ID token is still valid");
return Uni.createFrom().item(((TokenAutoRefreshException) t).getSecurityIdentity());
return Uni.createFrom().item(fallback);
} else {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.eclipse.microprofile.jwt.Claims;
import org.jboss.logging.Logger;
import org.jose4j.lang.UnresolvableKeyException;

Expand Down Expand Up @@ -99,25 +100,57 @@ private Uni<SecurityIdentity> validateAllTokensWithOidcServer(RoutingContext ver
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>>() {
@Override
public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
if (resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo
&& isOpaqueAccessToken(vertxContext, request, resolvedContext)) {
// UserInfo has to be acquired first as a precondition for verifying opaque access tokens.
// 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));
}
return validateTokenWithUserInfoAndCreateIdentity(vertxContext, request, resolvedContext, userInfo);
}
return validateTokenWithOidcServer(vertxContext, request, resolvedContext, userInfo);
}
});
});
} else {
// Verify Code Flow access token first if it is available and has to be verified.
// It may be refreshed if it has or has nearly expired
Uni<TokenVerificationResult> codeAccessTokenUni = verifyCodeFlowAccessTokenUni(vertxContext, request,
resolvedContext,
null);
return codeAccessTokenUni.onItemOrFailure().transformToUni(
new BiFunction<TokenVerificationResult, Throwable, Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> apply(TokenVerificationResult codeAccessTokenResult, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
if (codeAccessTokenResult != null) {
if (tokenAutoRefreshPrepared(codeAccessTokenResult, vertxContext,
resolvedContext.oidcConfig)) {
return Uni.createFrom().failure(new TokenAutoRefreshException(null));
}
vertxContext.put(CODE_ACCESS_TOKEN_RESULT, codeAccessTokenResult);
}
return getUserInfoAndCreateIdentity(vertxContext, request, resolvedContext);
}
});

}
}

private Uni<SecurityIdentity> validateTokenWithOidcServer(RoutingContext vertxContext, TokenAuthenticationRequest request,
private Uni<SecurityIdentity> validateTokenWithUserInfoAndCreateIdentity(RoutingContext vertxContext,
TokenAuthenticationRequest request,
TenantConfigContext resolvedContext, UserInfo userInfo) {

Uni<TokenVerificationResult> codeAccessTokenUni = verifyCodeFlowAccessTokenUni(vertxContext, request, resolvedContext,
userInfo);

Expand All @@ -138,6 +171,38 @@ public Uni<SecurityIdentity> apply(TokenVerificationResult codeAccessToken, Thro
});
}

private Uni<SecurityIdentity> getUserInfoAndCreateIdentity(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>>() {
@Override
public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
return createSecurityIdentityWithOidcServer(vertxContext, request, resolvedContext, userInfo);
}
});
}

private boolean isOpaqueAccessToken(RoutingContext vertxContext, TokenAuthenticationRequest request,
TenantConfigContext resolvedContext) {
if (request.getToken() instanceof AccessTokenCredential) {
return ((AccessTokenCredential) request.getToken()).isOpaque();
} else if (request.getToken() instanceof IdTokenCredential
&& (resolvedContext.oidcConfig.authentication.verifyAccessToken
|| resolvedContext.oidcConfig.roles.source.orElse(null) == Source.accesstoken)) {
final String codeAccessToken = (String) vertxContext.get(OidcConstants.ACCESS_TOKEN_VALUE);
return OidcUtils.isOpaqueToken(codeAccessToken);
}
return false;
}

private Uni<SecurityIdentity> createSecurityIdentityWithOidcServer(RoutingContext vertxContext,
TokenAuthenticationRequest request, TenantConfigContext resolvedContext, final UserInfo userInfo) {
Uni<TokenVerificationResult> tokenUni = null;
Expand Down Expand Up @@ -178,7 +243,10 @@ public Uni<SecurityIdentity> apply(TokenVerificationResult result, Throwable t)
userInfo);
SecurityIdentity securityIdentity = validateAndCreateIdentity(vertxContext, tokenCred,
resolvedContext, tokenJson, rolesJson, userInfo, result.introspectionResult);
if (tokenAutoRefreshPrepared(tokenJson, vertxContext, resolvedContext.oidcConfig)) {
// 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 (tokenCred instanceof IdTokenCredential
&& tokenAutoRefreshPrepared(result, vertxContext, resolvedContext.oidcConfig)) {
return Uni.createFrom().failure(new TokenAutoRefreshException(securityIdentity));
} else {
return Uni.createFrom().item(securityIdentity);
Expand All @@ -192,7 +260,7 @@ public Uni<SecurityIdentity> apply(TokenVerificationResult result, Throwable t)
return Uni.createFrom()
.failure(new AuthenticationFailedException("JWT token can not be converted to JSON"));
} else {
// Opaque Bearer Access Token
// ID Token or Bearer access token has been introspected
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.addCredential(tokenCred);
OidcUtils.setSecurityIdentityUserInfo(builder, userInfo);
Expand Down Expand Up @@ -238,7 +306,14 @@ public String getName() {
OidcUtils.setBlockingApiAttribute(builder, vertxContext);
OidcUtils.setTenantIdAttribute(builder, resolvedContext.oidcConfig);
OidcUtils.setRoutingContextAttribute(builder, vertxContext);
return Uni.createFrom().item(builder.build());
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 (tokenCred instanceof IdTokenCredential
&& tokenAutoRefreshPrepared(result, vertxContext, resolvedContext.oidcConfig)) {
return Uni.createFrom().failure(new TokenAutoRefreshException(identity));
}
return Uni.createFrom().item(identity);
}
}
});
Expand All @@ -248,17 +323,23 @@ private static boolean isInternalIdToken(TokenAuthenticationRequest request) {
return (request.getToken() instanceof IdTokenCredential) && ((IdTokenCredential) request.getToken()).isInternal();
}

private static boolean tokenAutoRefreshPrepared(JsonObject tokenJson, RoutingContext vertxContext,
private static boolean tokenAutoRefreshPrepared(TokenVerificationResult result, RoutingContext vertxContext,
OidcTenantConfig oidcConfig) {
if (tokenJson != null
&& oidcConfig.token.refreshExpired
if (result != null && oidcConfig.token.refreshExpired
&& oidcConfig.token.getRefreshTokenTimeSkew().isPresent()
&& vertxContext.get(REFRESH_TOKEN_GRANT_RESPONSE) != Boolean.TRUE
&& vertxContext.get(NEW_AUTHENTICATION) != Boolean.TRUE) {
final long refreshTokenTimeSkew = oidcConfig.token.getRefreshTokenTimeSkew().get().getSeconds();
final long expiry = tokenJson.getLong("exp");
final long now = System.currentTimeMillis() / 1000;
return now + refreshTokenTimeSkew > expiry;
Long expiry = null;
if (result.localVerificationResult != null) {
expiry = result.localVerificationResult.getLong(Claims.exp.name());
} else if (result.introspectionResult != null) {
expiry = result.introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP);
}
if (expiry != null) {
final long refreshTokenTimeSkew = oidcConfig.token.getRefreshTokenTimeSkew().get().getSeconds();
final long now = System.currentTimeMillis() / 1000;
return now + refreshTokenTimeSkew > expiry;
}
}
return false;
}
Expand Down
Loading