Skip to content

Commit

Permalink
Merge pull request quarkusio#10417 from sberyozkin/oidc_user_info
Browse files Browse the repository at this point in the history
OIDC UserInfo support
  • Loading branch information
sberyozkin authored Jul 8, 2020
2 parents ef2ea5a + 8562266 commit 91b6981
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,12 @@ public static Roles fromClaimPathAndSeparator(String path, String sep) {
@ConfigItem
public Optional<String> roleClaimSeparator = Optional.empty();

/**
* Source of the principal roles.
*/
@ConfigItem
public Optional<Source> source = Optional.empty();

public Optional<String> getRoleClaimPath() {
return roleClaimPath;
}
Expand All @@ -474,6 +480,33 @@ public Optional<String> getRoleClaimSeparator() {
public void setRoleClaimSeparator(String roleClaimSeparator) {
this.roleClaimSeparator = Optional.of(roleClaimSeparator);
}

public Optional<Source> getSource() {
return source;
}

public void setSource(Source source) {
this.source = Optional.of(source);
}

// Source of the principal roles
public static enum Source {
/**
* ID Token - the default value for the 'web-app' applications.
*/
idtoken,

/**
* Access Token - the default and only supported value for the 'service' applications;
* can also be used as the source of roles for the 'web-app' applications.
*/
accesstoken,

/**
* User Info - only supported for the "web-app" applications
*/
userinfo
}
}

/**
Expand Down Expand Up @@ -507,6 +540,15 @@ public static class Authentication {
@ConfigItem(defaultValue = "true")
public boolean removeRedirectParameters = true;

/**
* Both ID and access tokens are verified as part of the authorization code flow and every time
* these tokens are retrieved from the user session. One should disable the access token verification if
* it is only meant to be propagated to the downstream services.
* Note the ID token will always be verified.
*/
@ConfigItem(defaultValue = "true")
public boolean verifyAccessToken = true;

/**
* Force 'https' as the 'redirect_uri' parameter scheme when running behind an SSL terminating reverse proxy.
* This property, if enabled, will also affect the logout `post_logout_redirect_uri` and the local redirect requests.
Expand All @@ -533,6 +575,12 @@ public static class Authentication {
@ConfigItem
public Optional<String> cookiePath = Optional.empty();

/**
* If this property is set to 'true' then an OIDC UserInfo endpoint will be called
*/
@ConfigItem(defaultValue = "false")
public boolean userInfoRequired;

public Optional<String> getRedirectPath() {
return redirectPath;
}
Expand Down Expand Up @@ -580,6 +628,30 @@ public Optional<String> getCookiePath() {
public void setCookiePath(String cookiePath) {
this.cookiePath = Optional.of(cookiePath);
}

public boolean isUserInfoRequired() {
return userInfoRequired;
}

public void setUserInfoRequired(boolean userInfoRequired) {
this.userInfoRequired = userInfoRequired;
}

public boolean isRemoveRedirectParameters() {
return removeRedirectParameters;
}

public void setRemoveRedirectParameters(boolean removeRedirectParameters) {
this.removeRedirectParameters = removeRedirectParameters;
}

public boolean isVerifyAccessToken() {
return verifyAccessToken;
}

public void setVerifyAccessToken(boolean verifyAccessToken) {
this.verifyAccessToken = verifyAccessToken;
}
}

@ConfigGroup
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.quarkus.oidc;

import java.io.StringReader;

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

public class UserInfo {

private JsonObject json;

public UserInfo() {
}

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

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);
}

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 @@ -94,6 +94,7 @@ public Uni<SecurityIdentity> authenticate(RoutingContext context,
String refreshToken = tokens[2];

TenantConfigContext configContext = resolver.resolve(context, true);
context.put("access_token", accessToken);
return authenticate(identityProviderManager, new IdTokenCredential(idToken, context))
.map(new Function<SecurityIdentity, SecurityIdentity>() {
@Override
Expand Down Expand Up @@ -122,8 +123,8 @@ public SecurityIdentity apply(Throwable throwable) {
throw new AuthenticationCompletionException(cause);
}
LOG.debug("Token has expired, trying to refresh it");
SecurityIdentity identity = trySilentRefresh(configContext, idToken, refreshToken, context,
identityProviderManager);
SecurityIdentity identity = trySilentRefresh(configContext,
refreshToken, context, identityProviderManager);
if (identity == null) {
LOG.debug("SecurityIdentity is null after a token refresh");
throw new AuthenticationCompletionException();
Expand Down Expand Up @@ -276,8 +277,9 @@ public void accept(UniEmitter<? super SecurityIdentity> uniEmitter) {
}
uniEmitter.fail(new AuthenticationCompletionException(userAsyncResult.cause()));
} else {
AccessToken result = AccessToken.class.cast(userAsyncResult.result());
final AccessToken result = AccessToken.class.cast(userAsyncResult.result());

context.put("access_token", result.opaqueAccessToken());
authenticate(identityProviderManager, new IdTokenCredential(result.opaqueIdToken(), context))
.subscribe().with(new Consumer<SecurityIdentity>() {
@Override
Expand All @@ -287,7 +289,8 @@ public void accept(SecurityIdentity identity) {
uniEmitter.fail(new AuthenticationCompletionException());
}
processSuccessfulAuthentication(context, configContext, result, identity);
if (configContext.oidcConfig.authentication.removeRedirectParameters

if (configContext.oidcConfig.authentication.isRemoveRedirectParameters()
&& context.request().query() != null) {
String finalRedirectUri = buildUriWithoutQueryParams(context);
if (finalUserQuery != null) {
Expand Down Expand Up @@ -331,6 +334,7 @@ private String signJwtWithClientSecret(OidcTenantConfig cfg) {

private void processSuccessfulAuthentication(RoutingContext context, TenantConfigContext configContext,
AccessToken result, SecurityIdentity securityIdentity) {

removeCookie(context, configContext, getSessionCookieName(configContext));

String cookieValue = new StringBuilder(result.opaqueIdToken())
Expand Down Expand Up @@ -430,7 +434,7 @@ private boolean isLogout(RoutingContext context, TenantConfigContext configConte
return false;
}

private SecurityIdentity trySilentRefresh(TenantConfigContext configContext, String idToken, String refreshToken,
private SecurityIdentity trySilentRefresh(TenantConfigContext configContext, String refreshToken,
RoutingContext context, IdentityProviderManager identityProviderManager) {

Uni<SecurityIdentity> cf = Uni.createFrom().emitter(new Consumer<UniEmitter<? super SecurityIdentity>>() {
Expand All @@ -445,6 +449,7 @@ public void accept(UniEmitter<? super SecurityIdentity> emitter) {
@Override
public void handle(AsyncResult<Void> result) {
if (result.succeeded()) {
context.put("access_token", token.opaqueAccessToken());
authenticate(identityProviderManager,
new IdTokenCredential(token.opaqueIdToken(), context))
.subscribe().with(new Consumer<SecurityIdentity>() {
Expand Down Expand Up @@ -518,5 +523,4 @@ private static String getPostLogoutCookieName(TenantConfigContext configContext)
private static String getCookieSuffix(TenantConfigContext configContext) {
return !"Default".equals(configContext.oidcConfig.tenantId.get()) ? "_" + configContext.oidcConfig.tenantId.get() : "";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ private TenantConfigContext getTenantConfigFromTenantResolver(RoutingContext con

boolean isBlocking(RoutingContext context) {
TenantConfigContext resolver = resolve(context, false);
return resolver != null && (resolver.auth == null || resolver.oidcConfig.token.refreshExpired);
return resolver != null
&& (resolver.auth == null || resolver.oidcConfig.token.refreshExpired
|| resolver.oidcConfig.authentication.userInfoRequired);
}

private TenantConfigContext getTenantConfigFromConfigResolver(RoutingContext context, boolean create) {
Expand Down
Loading

0 comments on commit 91b6981

Please sign in to comment.