Skip to content
Open
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 @@ -27,6 +27,7 @@
import java.net.URLEncoder;

import javax.script.ScriptContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
Expand Down Expand Up @@ -55,7 +56,7 @@

/**
* Authenticate user trough an OpenID Connect provider.
*
*
* @version $Id$
*/
public class OIDCAuthServiceImpl extends XWikiAuthServiceImpl
Expand Down Expand Up @@ -85,6 +86,21 @@ public XWikiUser checkAuth(XWikiContext context) throws XWikiException
XWikiUser user = super.checkAuth(context);

if (user == null) {
// obtain user from authorization header
if (configuration.isAllowAccessToken()) {
HttpServletRequest request = context.getRequest().getHttpServletRequest();
String idTokenHeader = request.getHeader("X-Id-Token");
String accessTokenHeader = request.getHeader("X-Access-Token");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to receive the access token as a Bearer Authorization header ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could certainly be done this way. But for the Authorization header I would expect that single header to be sufficient for authorization, thou we would have to combine the two tokens. And sending the id token is non-standard anyways. I would prefer to leave it as-is.


if (idTokenHeader != null && accessTokenHeader != null) {
Copy link
Member

@tmortagne tmortagne Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to me the ID token should be optional (I'm not even sure it's needed at all actually as there is probably a way to request an ID token from the provider, with the access token).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this and have not found a way to get one.

// validate JWT
user = users.checkAccessToken(idTokenHeader, accessTokenHeader);
if (user != null) {
return user;
}
}
}

LOGGER.debug("No user could be found in the session, starting an OpenID Connect authentication");

// Try OIDC if there is no already authenticated user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ public Map<String, Set<String>> getProviderMapping()

public static final String PROP_SKIPPED = "oidc.skipped";

/**
* @since @since 2.19.0
*/
public static final String PROP_ALLOW_ACCESS_TOKEN = "oidc.allow_access_token";

/**
* @since 1.13
*/
Expand Down Expand Up @@ -630,6 +635,14 @@ public Map<String, String> getUserMapping()
return getMap(PROP_USER_MAPPING);
}

/**
* @since 2.19.0
*/
public boolean isAllowAccessToken()
{
return getProperty(PROP_ALLOW_ACCESS_TOKEN, false);
}

public String getProvider()
{
String provider = getProperty(PROP_PROVIDER, String.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.inject.Inject;
Expand All @@ -56,6 +58,8 @@
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.context.concurrent.ExecutionContextRunnable;
import org.xwiki.contrib.oidc.OIDCUserInfo;
import org.xwiki.contrib.oidc.auth.internal.OIDCClientConfiguration.GroupMapping;
Expand All @@ -73,14 +77,27 @@
import org.xwiki.query.QueryException;
import org.xwiki.user.SuperAdminUserReference;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.oauth2.sdk.GeneralException;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.LogoutRequest;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
Expand All @@ -96,6 +113,7 @@
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.classes.BaseClass;
import com.xpn.xwiki.user.api.XWikiUser;
import com.xpn.xwiki.web.XWikiRequest;

/**
Expand All @@ -106,7 +124,7 @@
*/
@Component(roles = OIDCUserManager.class)
@Singleton
public class OIDCUserManager
public class OIDCUserManager implements Initializable
{
@Inject
private Provider<XWikiContext> xcontextProvider;
Expand Down Expand Up @@ -143,6 +161,81 @@ public class OIDCUserManager

private static final String XWIKI_GROUP_PREFIX = "XWiki.";

private ConfigurableJWTProcessor<SecurityContext> jwtProcessor;

private Cache<String, DocumentReference> userByAccessTokenCache =
CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();

@Override
public void initialize() throws InitializationException
{

try {
ClientProvider clientProvider = configuration.getClientProvider();
if (clientProvider != null) {
jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWT")));
JWKSource<SecurityContext> keySource;
keySource =
JWKSourceBuilder.create(configuration.getClientProvider().getMetadata().getJWKSetURI().toURL())
.retrying(true).build();

JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;
// Configure the JWT processor with a key selector to feed matching public
// RSA keys sourced from the JWK set URL
JWSKeySelector<SecurityContext> keySelector =
new JWSVerificationKeySelector<>(expectedJWSAlg, keySource);
jwtProcessor.setJWSKeySelector(keySelector);
}
} catch (IOException | GeneralException | URISyntaxException e) {
throw new RuntimeException(e);
}
}

public XWikiUser checkAccessToken(String idTokenHeader, String accessTokenHeader)
{
try {
DocumentReference ref = userByAccessTokenCache.get(accessTokenHeader, () -> {
// check id token signature
IDTokenClaimsSet idToken = new IDTokenClaimsSet(jwtProcessor.process(idTokenHeader, null));

if (!idToken.getAudience().stream().anyMatch(aud -> {
try {
return aud.toString().equals(configuration.getClientID().getValue());
} catch (Exception e) {
throw new RuntimeException(e);
}
}))
throw new OIDCException("ID Token Audience mismatch. Expected " + configuration.getClientID()
+ ", found " + idToken.getAudience());

AccessToken accessToken = new BearerAccessToken(accessTokenHeader);

UserInfo userInfo = getUserInfo(accessToken);

if (!idToken.getSubject().equals(userInfo.getSubject())) {
throw new OIDCException("Subject of ID Token " + idToken.getSubject()
+ " does not match subject of user info retrieved using access token " + userInfo.getSubject());
}

// also checks if the user is allowed to access the wiki
updateUser(idToken, userInfo, accessToken);

StringSubstitutor substitutor = getSubstitutor(idToken, userInfo);
String formattedSubject = formatSubject(substitutor);
XWikiDocument userDocument = store.searchDocument(idToken.getIssuer().getValue(), formattedSubject);

return userDocument.getDocumentReference();
});
if (ref == null)
return null;
return new XWikiUser(ref);
} catch (ExecutionException e) {
logger.error("Error while validating access token", e);
return null;
}
}

public void updateUserInfoAsync()
{
final IDTokenClaimsSet idToken = this.configuration.getIdToken();
Expand Down Expand Up @@ -282,6 +375,7 @@ private void checkAllowedGroups(List<String> providerGroups) throws OIDCExceptio
}
}

@SuppressWarnings("unchecked")
private <T> T getClaim(String claim, ClaimsSet claims)
{
T value = (T) claims.getClaim(claim);
Expand Down Expand Up @@ -330,10 +424,7 @@ public SimplePrincipal updateUser(IDTokenClaimsSet idToken, UserInfo userInfo, A
// Check allowed/forbidden groups
checkAllowedGroups(providerGroups);

Map<String, String> formatMap = createFormatMap(idToken, userInfo);
// Change the default StringSubstitutor behavior to produce an empty String instead of an unresolved pattern by
// default
StringSubstitutor substitutor = new StringSubstitutor(new OIDCStringLookup(formatMap));
StringSubstitutor substitutor = getSubstitutor(idToken, userInfo);

String formattedSubject = formatSubject(substitutor);

Expand Down Expand Up @@ -488,6 +579,16 @@ public SimplePrincipal updateUser(IDTokenClaimsSet idToken, UserInfo userInfo, A
return new SimplePrincipal(userDocument.getPrefixedFullName());
}

private StringSubstitutor getSubstitutor(IDTokenClaimsSet idToken, UserInfo userInfo) throws MalformedURLException
{
Map<String, String> formatMap = createFormatMap(idToken, userInfo);

// Change the default StringSubstitutor behavior to produce an empty String
// instead of an unresolved pattern by
// default
return new StringSubstitutor(new OIDCStringLookup(formatMap));
}

private void updateUserMapping(XWikiDocument userDocument, BaseClass userClass, BaseObject userObject,
XWikiContext xcontext, StringSubstitutor substitutor)
{
Expand Down