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

Resolve HttpCredentialTransport dynamically #22483

Merged
merged 1 commit into from
Feb 7, 2022
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
58 changes: 57 additions & 1 deletion docs/src/main/asciidoc/security-customization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,69 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism
}

@Override
public HttpCredentialTransport getCredentialTransport() {
public Uni<HttpCredentialTransport> getCredentialTransport() {
return delegate.getCredentialTransport();
}

}
----

== Dealing with more than one HttpAuthenticationMechanism

More than one `HttpAuthenticationMechanism` can be combined, for example, the built-in `Basic` or `JWT` mechanism provided by `quarkus-smallrye-jwt` has to be used to verify the service clients credentials passed as the HTTP `Authorization` `Basic` or `Bearer` scheme values while the `Authorization Code` mechanism provided by `quarkus-oidc` has to be used to authenticate the users with `Keycloak` or other `OpenId Connect` providers.

In such cases the mechanisms are asked to verify the credentials in turn until a `SecurityIdentity` is created. The mechanisms are sorted in the descending order using their priority. `Basic` authentication mechanism has the highest priority of `2000`, followed by the `Authorization Code` one with the priority of `1001`, with all other mechanisms provided by Quarkus having the priority of `1000`.

If no credentials are provided then the mechanism specific challenge is created, for example, `401` status is returned by either `Basic` or `JWT` mechanisms, URL redirecting the user to the `OpenId Connect` provider is returned by `quarkus-oidc`, etc.

So if `Basic` and `Authorization Code` mechanisms are combined then `401` will be returned if no credentials are provided and if `JWT` and `Authorization Code` mechanisms are combined then a redirect URL will be returned.

In some cases such a default logic of selecting the challenge is exactly what is required by a given application but sometimes it may not meet the requirements. In such cases (or indeed in other similar cases where you'd like to change the order in which the mechanisms are asked to handle the current authentication or challenge request), you can create a custom mechanism and choose which mechanism should create a challenge, for example:

[source,java]
----
@ApplicationScoped
public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism {

private static final Logger LOG = LoggerFactory.getLogger(CustomAwareJWTAuthMechanism.class);

@Inject
JWTAuthMechanism jwt;

@Inject
OidcAuthenticationMechanism oidc;

@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
return selectBetweenJwtAndOidc(context).authenticate(context, identityProviderManager);
}

@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return selectBetweenJwtAndOidcChallenge(context).getChallenge(context);
}

@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return selectBetweenJwtAndOidc(context).getCredentialTypes();
}

@Override
public HttpCredentialTransport getCredentialTransport(RoutingContext context) {
return selectBetweenJwtAndOidc(context).getCredentialTransport();
}

private HttpAuthenticationMechanism selectBetweenJwtAndOidc(RoutingContext context) {
....
}

private HttpAuthenticationMechanism selectBetweenJwtAndOidcChallenge(RoutingContext context) {
// for example, if no `Authorization` header is available and no `code` parameter is provided - use `jwt` to create a challenge
}

}
----

[[security-identity-customization]]
== Security Identity Customization

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

abstract class AbstractOidcAuthenticationMechanism {
protected DefaultTenantConfigResolver resolver;
private HttpAuthenticationMechanism parent;

protected Uni<SecurityIdentity> authenticate(IdentityProviderManager identityProviderManager,
RoutingContext context, TokenCredential token) {
context.put(HttpAuthenticationMechanism.class.getName(), parent);
return identityProviderManager.authenticate(HttpSecurityUtils.setRoutingContextAttribute(
new TokenAuthenticationRequest(token), context));
}

void setResolver(DefaultTenantConfigResolver resolver) {
void init(HttpAuthenticationMechanism parent, DefaultTenantConfigResolver resolver) {
this.parent = parent;
this.resolver = resolver;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,19 @@

@ApplicationScoped
public class OidcAuthenticationMechanism implements HttpAuthenticationMechanism {
private static HttpCredentialTransport OIDC_SERVICE_TRANSPORT = new HttpCredentialTransport(
HttpCredentialTransport.Type.AUTHORIZATION, OidcConstants.BEARER_SCHEME);
private static HttpCredentialTransport OIDC_WEB_APP_TRANSPORT = new HttpCredentialTransport(
HttpCredentialTransport.Type.AUTHORIZATION_CODE, OidcConstants.CODE_FLOW_CODE);

private final BearerAuthenticationMechanism bearerAuth = new BearerAuthenticationMechanism();
private final CodeAuthenticationMechanism codeAuth = new CodeAuthenticationMechanism();

private final DefaultTenantConfigResolver resolver;

public OidcAuthenticationMechanism(DefaultTenantConfigResolver resolver) {
this.resolver = resolver;
this.bearerAuth.setResolver(resolver);
this.codeAuth.setResolver(resolver);
this.bearerAuth.init(this, resolver);
this.codeAuth.init(this, resolver);
}

@Override
Expand Down Expand Up @@ -91,10 +94,18 @@ public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
}

@Override
public HttpCredentialTransport getCredentialTransport() {
//not 100% correct, but enough for now
//if OIDC is present we don't really want another bearer mechanism
return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, OidcConstants.BEARER_SCHEME);
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
setTenantIdAttribute(context);
return resolve(context).onItem().transform(new Function<OidcTenantConfig, HttpCredentialTransport>() {
@Override
public HttpCredentialTransport apply(OidcTenantConfig oidcTenantConfig) {
if (!oidcTenantConfig.tenantEnabled) {
return null;
}
return isWebApp(context, oidcTenantConfig) ? OIDC_WEB_APP_TRANSPORT
: OIDC_SERVICE_TRANSPORT;
}
});
}

private static void setTenantIdAttribute(RoutingContext context) {
Expand All @@ -119,4 +130,9 @@ private static void setTenantIdAttribute(RoutingContext context, String cookiePr
context.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId);
}
}

@Override
public int getPriority() {
return HttpAuthenticationMechanism.DEFAULT_PRIORITY + 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public Class<TokenAuthenticationRequest> getRequestType() {
@Override
public Uni<SecurityIdentity> authenticate(TokenAuthenticationRequest request,
AuthenticationRequestContext context) {
if (!(request.getToken() instanceof AccessTokenCredential || request.getToken() instanceof IdTokenCredential)) {
return Uni.createFrom().nullItem();
}
RoutingContext vertxContext = HttpSecurityUtils.getRoutingContextAttribute(request);
vertxContext.put(AuthenticationRequestContext.class.getName(), context);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.security.credential.TokenCredential;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.runtime.AnonymousIdentityProvider;
import io.quarkus.security.runtime.QuarkusIdentityProviderManagerImpl;
import io.quarkus.smallrye.jwt.runtime.auth.JsonWebTokenCredential;
import io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator;
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
Expand Down Expand Up @@ -43,7 +43,7 @@ public void execute(Runnable command) {
.addProvider(jwtValidator).build();

String jwt = TokenUtils.generateTokenString("/Token1.json", pk1Priv, "testTokenRealm");
TokenAuthenticationRequest tokenEvidence = new TokenAuthenticationRequest(new TokenCredential(jwt, "bearer"));
TokenAuthenticationRequest tokenEvidence = new TokenAuthenticationRequest(new JsonWebTokenCredential(jwt));
SecurityIdentity securityIdentity = authenticator.authenticate(tokenEvidence).await().indefinitely();
Assertions.assertNotNull(securityIdentity);
Assertions.assertEquals("[email protected]", securityIdentity.getPrincipal().getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.jwt.auth.AbstractBearerTokenExtractor;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.mutiny.Uni;
Expand All @@ -42,8 +43,10 @@ public Uni<SecurityIdentity> authenticate(RoutingContext context,
IdentityProviderManager identityProviderManager) {
String jwtToken = new VertxBearerTokenExtractor(authContextInfo, context).getBearerToken();
if (jwtToken != null) {
context.put(HttpAuthenticationMechanism.class.getName(), this);
return identityProviderManager
.authenticate(new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken)));
.authenticate(HttpSecurityUtils.setRoutingContextAttribute(
new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken)), context));
}
return Uni.createFrom().optional(Optional.empty());
}
Expand All @@ -53,7 +56,7 @@ public Uni<ChallengeData> getChallenge(RoutingContext context) {
ChallengeData result = new ChallengeData(
HttpResponseStatus.UNAUTHORIZED.code(),
HttpHeaderNames.WWW_AUTHENTICATE,
"Bearer");
BEARER);
return Uni.createFrom().item(result);
}

Expand Down Expand Up @@ -93,19 +96,20 @@ public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
}

@Override
public HttpCredentialTransport getCredentialTransport() {
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
final String tokenHeaderName = authContextInfo.getTokenHeader();
if (COOKIE_HEADER.equals(tokenHeaderName)) {
String tokenCookieName = authContextInfo.getTokenCookie();

if (tokenCookieName == null) {
tokenCookieName = BEARER;
}
return new HttpCredentialTransport(HttpCredentialTransport.Type.COOKIE, tokenCookieName);
return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.COOKIE, tokenCookieName));
} else if (AUTHORIZATION_HEADER.equals(tokenHeaderName)) {
return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BEARER);
return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BEARER));
} else {
return new HttpCredentialTransport(HttpCredentialTransport.Type.OTHER_HEADER, tokenHeaderName);
return Uni.createFrom()
.item(new HttpCredentialTransport(HttpCredentialTransport.Type.OTHER_HEADER, tokenHeaderName));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public Class<TokenAuthenticationRequest> getRequestType() {
@Override
public Uni<SecurityIdentity> authenticate(TokenAuthenticationRequest request,
AuthenticationRequestContext context) {
if (!(request.getToken() instanceof JsonWebTokenCredential)) {
return Uni.createFrom().nullItem();
}
if (!blockingAuthentication) {
return Uni.createFrom().emitter(new Consumer<UniEmitter<? super SecurityIdentity>>() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ public Uni<SecurityIdentity> authenticate(RoutingContext context,
UsernamePasswordAuthenticationRequest credential = new UsernamePasswordAuthenticationRequest(userName,
new PasswordCredential(password));
HttpSecurityUtils.setRoutingContextAttribute(credential, context);
context.put(HttpAuthenticationMechanism.class.getName(), this);

return identityProviderManager.authenticate(credential);
}

Expand Down Expand Up @@ -192,8 +194,8 @@ public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
}

@Override
public HttpCredentialTransport getCredentialTransport() {
return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BASIC);
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BASIC));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,12 @@ public Uni<SecurityIdentity> authenticate(RoutingContext context,

if (context.normalizedPath().endsWith(postLocation) && context.request().method().equals(HttpMethod.POST)) {
//we always re-auth if it is a post to the auth URL
context.put(HttpAuthenticationMechanism.class.getName(), this);
return runFormAuth(context, identityProviderManager);
} else {
PersistentLoginManager.RestoreResult result = loginManager.restore(context);
if (result != null) {
context.put(HttpAuthenticationMechanism.class.getName(), this);
Uni<SecurityIdentity> ret = identityProviderManager
.authenticate(HttpSecurityUtils
.setRoutingContextAttribute(new TrustedAuthenticationRequest(result.getPrincipal()), context));
Expand Down Expand Up @@ -194,7 +196,7 @@ public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
}

@Override
public HttpCredentialTransport getCredentialTransport() {
return new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM);
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,26 @@ default Uni<Boolean> sendChallenge(RoutingContext context) {
}

/**
* The credential transport, used to make sure multiple incompatible mechanisms are not installed
* The credential transport, used for finding the best candidate for authenticating and challenging when more than one
* mechanism is installed.
* and finding the best candidate for issuing a challenge when more than one mechanism is installed.
*
* May be null if this mechanism cannot interfere with other mechanisms
*/
HttpCredentialTransport getCredentialTransport();
@Deprecated
default HttpCredentialTransport getCredentialTransport() {
throw new UnsupportedOperationException();
}

/**
* The credential transport, used for finding the best candidate for authenticating and challenging when more than one
* mechanism is installed.
*
* May be null if this mechanism cannot interfere with other mechanisms
*/
default Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
throw new UnsupportedOperationException();
}

class ChallengeSender implements Function<ChallengeData, Boolean> {

Expand Down
Loading