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 @@ -15,6 +15,7 @@
*/
package dev.dsf.bpe.v2.client.oidc;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -174,6 +175,11 @@ public String getUse()
return delegate.getUse();
}

public List<String> getKeyOps()
{
return delegate.getKeyOps();
}

@Override
public String getN()
{
Expand Down Expand Up @@ -238,6 +244,13 @@ public String getUse()
return delegate.getUse();
}

@Override
public List<String> getKeyOps()
{
// TODO add getKeyOps method to JwksKey in API v3
return delegate instanceof JwksKeyApiDelegate d ? d.getKeyOps() : List.of();
}

@Override
public String getN()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package dev.dsf.bpe.api.client.oidc;

import java.util.List;
import java.util.Optional;
import java.util.Set;

Expand All @@ -32,6 +33,8 @@ public static interface JwksKey

String getUse();

List<String> getKeyOps();

String getN();

String getE();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public static class JwksKeyImpl implements JwksKey
private final String alg;
private final String crv;
private final String use;
private final List<String> keyOps;
private final String n;
private final String e;
private final String x;
Expand All @@ -48,14 +49,15 @@ public static class JwksKeyImpl implements JwksKey
@JsonCreator
public JwksKeyImpl(@JsonProperty("kid") String kid, @JsonProperty("kty") String kty,
@JsonProperty("alg") String alg, @JsonProperty("crv") String crv, @JsonProperty("use") String use,
@JsonProperty("n") String n, @JsonProperty("e") String e, @JsonProperty("x") String x,
@JsonProperty("y") String y)
@JsonProperty("key_ops") List<String> keyOps, @JsonProperty("n") String n, @JsonProperty("e") String e,
@JsonProperty("x") String x, @JsonProperty("y") String y)
{
this.kid = kid;
this.kty = kty;
this.alg = alg;
this.crv = crv;
this.use = use;
this.keyOps = keyOps == null ? List.of() : List.copyOf(keyOps);
this.n = n;
this.e = e;
this.x = x;
Expand Down Expand Up @@ -92,6 +94,12 @@ public String getUse()
return use;
}

@Override
public List<String> getKeyOps()
{
return keyOps;
}

@Override
public String getN()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,11 @@
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.IncorrectClaimException;
import com.auth0.jwt.exceptions.JWTDecodeException;
Expand All @@ -43,6 +40,8 @@
import dev.dsf.bpe.api.client.oidc.Configuration;
import dev.dsf.bpe.api.client.oidc.OidcClientException;
import dev.dsf.bpe.client.oidc.JwksImpl.JwksKeyImpl;
import dev.dsf.common.oidc.BaseJwtVerifier;
import dev.dsf.common.oidc.BaseJwtVerifierImpl;
import dev.dsf.common.oidc.BaseOidcClientJersey;
import dev.dsf.common.oidc.Jwks;
import dev.dsf.common.oidc.Jwks.JwksKey;
Expand All @@ -67,10 +66,11 @@ public class OidcClientJersey extends BaseOidcClientJersey

private final String clientId;
private final char[] clientSecret;
private final Duration notBeforeIssuedAtExpiresAtLeewaySeconds;
private final List<String> requiredAudiences = new ArrayList<>();
private final boolean verifyAuthorizedParty;

private final BaseJwtVerifier accessTokenVerifier;

/**
* @param baseUrl
* not <code>null</code>
Expand Down Expand Up @@ -99,7 +99,7 @@ public class OidcClientJersey extends BaseOidcClientJersey
* @param readTimeout
* not <code>null</code>
* @param logRequestsAndResponses
* @param notBeforeIssuedAtExpiresAtLeewaySeconds
* @param notBeforeIssuedAtExpiresAtLeeway
* not <code>null</code>
* @param requiredAudiences
* may be <code>null</code>
Expand All @@ -108,19 +108,20 @@ public class OidcClientJersey extends BaseOidcClientJersey
public OidcClientJersey(String baseUrl, String discoveryPath, String clientId, char[] clientSecret,
KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, String proxySchemeHostPort,
String proxyUserName, char[] proxyPassword, String userAgentValue, Duration connectTimeout,
Duration readTimeout, boolean logRequestsAndResponses, Duration notBeforeIssuedAtExpiresAtLeewaySeconds,
Duration readTimeout, boolean logRequestsAndResponses, Duration notBeforeIssuedAtExpiresAtLeeway,
List<String> requiredAudiences, boolean verifyAuthorizedParty)
{
super(baseUrl, discoveryPath, trustStore, keyStore, keyStorePassword, proxySchemeHostPort, proxyUserName,
proxyPassword, userAgentValue, connectTimeout, readTimeout, logRequestsAndResponses);

this.clientId = clientId;
this.clientSecret = clientSecret;
this.notBeforeIssuedAtExpiresAtLeewaySeconds = Objects.requireNonNull(notBeforeIssuedAtExpiresAtLeewaySeconds,
"notBeforeIssuedAtExpiresAtLeewaySeconds");
if (requiredAudiences != null)
this.requiredAudiences.addAll(requiredAudiences);
this.verifyAuthorizedParty = verifyAuthorizedParty;

accessTokenVerifier = new BaseJwtVerifierImpl("Access", baseUrl, this::getJwks,
notBeforeIssuedAtExpiresAtLeeway);
}

private void logUnexpectedResponseAndClose(Response response)
Expand Down Expand Up @@ -176,64 +177,42 @@ private DecodedJWT verifyAndDecodeAccessToken(String accessToken, Jwks jwks) thr
{
try
{
DecodedJWT decoded = JWT.decode(accessToken);
String keyId = decoded.getKeyId();

if (keyId == null)
throw new OidcClientException("Access token has no kid property");

Optional<JwksKey> key = jwks.getKey(keyId);
if (key.isEmpty() || !key.get().use().equals("sig"))
throw new OidcClientException("Access token key with kid '" + keyId + "' and use 'sig' not in JWKS");
Verification v = accessTokenVerifier.createVerification(accessToken);

Optional<Algorithm> algorithm = key.flatMap(JwksKey::toAlgorithm);
if (algorithm.isEmpty())
{
throw new OidcClientException("Access token key with kid '" + keyId
+ "' has unsupported type (kty) / algorithm (alg) / key-size in JWKS");
}

try
{
Verification v = JWT.require(algorithm.get())
.acceptLeeway(notBeforeIssuedAtExpiresAtLeewaySeconds.getSeconds());
if (requiredAudiences.size() == 1)
v.withAudience(requiredAudiences.get(0));
else if (requiredAudiences.size() > 1)
v.withAudience(requiredAudiences.toArray(String[]::new));

if (requiredAudiences.size() == 1)
v.withAudience(requiredAudiences.get(0));
else if (requiredAudiences.size() > 1)
v.withAudience(requiredAudiences.toArray(String[]::new));
if (verifyAuthorizedParty)
v.withClaim("azp", clientId);

if (verifyAuthorizedParty)
v.withClaim("azp", clientId);

return v.build().verify(decoded);
}
catch (AlgorithmMismatchException e)
{
throw new OidcClientException(
"JWT verification failed: algorithm not '" + algorithm.get().getName() + "'", e);
}
catch (SignatureVerificationException e)
{
throw new OidcClientException("JWT verification failed: signature invalid", e);
}
catch (TokenExpiredException e)
{
throw new OidcClientException("JWT verification failed: token expired", e);
}
catch (IncorrectClaimException e)
{
throw new OidcClientException("JWT verification failed: claim contained unexpected value", e);
}
catch (JWTVerificationException e)
{
throw new OidcClientException("JWT verification failed", e);
}
return v.build().verify(accessToken);
}
catch (AlgorithmMismatchException e)
{
throw new OidcClientException("JWT verification failed: JWKS/token algorithm mismatch", e);
}
catch (SignatureVerificationException e)
{
throw new OidcClientException("JWT verification failed: signature invalid", e);
}
catch (TokenExpiredException e)
{
throw new OidcClientException("JWT verification failed: token expired", e);
}
catch (IncorrectClaimException e)
{
throw new OidcClientException("JWT verification failed: claim contained unexpected value", e);
}
catch (JWTDecodeException e)
{
throw new OidcClientException("Unable to parse access token", e);
}
catch (JWTVerificationException e)
{
throw new OidcClientException("JWT verification failed", e);
}
}

public OidcClientWithDecodedJwt asOidcClientWithDecodedJwt()
Expand Down Expand Up @@ -273,9 +252,8 @@ private OidcConfiguration toOidcConfiguration(Configuration configuration)

private dev.dsf.bpe.api.client.oidc.Jwks toJwksImpl(Jwks jwks)
{
return new JwksImpl(jwks.getKeys().stream().map(
k -> new JwksKeyImpl(k.kid(), k.kty(), k.alg(), k.crv(), k.use(), k.n(), k.e(), k.x(), k.y()))
.toList());
return new JwksImpl(jwks.getKeys().stream().map(k -> new JwksKeyImpl(k.kid(), k.kty(), k.alg(), k.crv(),
k.use(), k.keyOps(), k.n(), k.e(), k.x(), k.y())).toList());
}

private Configuration toConfigurationImpl(OidcConfiguration configuration)
Expand All @@ -287,7 +265,7 @@ private Configuration toConfigurationImpl(OidcConfiguration configuration)
private Jwks toJwks(dev.dsf.bpe.api.client.oidc.Jwks jwks)
{
return new Jwks(jwks.getKeys().stream().map(k -> new JwksKey(k.getKid(), k.getKty(), k.getAlg(),
k.getCrv(), k.getUse(), k.getN(), k.getE(), k.getX(), k.getY())).toList());
k.getCrv(), k.getUse(), k.getKeyOps(), k.getN(), k.getE(), k.getX(), k.getY())).toList());
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public AuthenticationState validateRequest(Request request, Response response, C

try
{
DecodedJWT jwt = jwtVerifier.verifyBackchannelLogout(logoutTokenField.getValue());
DecodedJWT jwt = jwtVerifier.verifyBackchannelLogoutToken(logoutTokenField.getValue());

if (!jwt.getClaims().containsKey("sub") && !jwt.getClaims().containsKey("sid"))
{
Expand All @@ -130,12 +130,12 @@ public AuthenticationState validateRequest(Request request, Response response, C

logger.debug("Invalidating session for sub/sid {}/{}", sub, sid);

HttpSession sessionBySub = sessionsBySub.get(sub);
HttpSession sessionBySub = sub == null ? null : sessionsBySub.get(sub);
if (sessionBySub != null)
sessionBySub.invalidate();

// session will have been removed if found by sub and invalidated
HttpSession sessionBySid = sessionsBySid.get(sid);
HttpSession sessionBySid = sid == null ? null : sessionsBySid.get(sid);
if (sessionBySid != null)
sessionBySid.invalidate();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ public abstract class AbstractJettyConfig extends AbstractCertificateAndProxyCon
@Value("${dev.dsf.server.auth.oidc.bearer.token.audience:#{null}}")
private String oidcBearerTokenAudience;

@Documentation(description = "OIDC bearer tokens time validation leeway for 'Not Before', 'Issued At' and 'Expires At' values")
@Value("${dev.dsf.server.auth.oidc.bearer.token.time.validation.leeway:PT10S}")
private String oidcBearerTokenNotBeforeIssuedAtExpiresAtLeeway;

@Documentation(description = "OIDC provider realm base url", example = "https://keycloak.test.com:8443/realms/example-realm-name")
@Value("${dev.dsf.server.auth.oidc.provider.realm.base.url:#{null}}")
private String oidcProviderRealmBaseUrl;
Expand Down Expand Up @@ -229,6 +233,10 @@ public abstract class AbstractJettyConfig extends AbstractCertificateAndProxyCon
@Value("${dev.dsf.server.auth.oidc.back.channel.logout.path:/back-channel-logout}")
private String oidcBackChannelPath;

@Documentation(description = "OIDC back-channel logout tokens time validation leeway for 'Not Before', 'Issued At' and 'Expires At' values")
@Value("${dev.dsf.server.auth.oidc.back.channel.logout.token.time.validation.leeway:PT10S}")
private String oidcBackChannelNotBeforeIssuedAtExpiresAtLeeway;

@Documentation(description = "Maximum inactivity period after which the server session for OIDC logins is invalidated; the access token may expire earlier, resulting in earlier session invalidation")
@Value("${dev.dsf.server.auth.oidc.session.timeout:PT30M}")
private String oidcSessionTimeout;
Expand Down Expand Up @@ -454,11 +462,23 @@ else if (oidcBackChannelPath == null)
sessionHandler.addEventListener(backChannelLogoutAuthenticator);
}

private Duration getOidcBearerTokenNotBeforeIssuedAtExpiresAtLeeway()
{
return assertPositive(Duration.parse(oidcBearerTokenNotBeforeIssuedAtExpiresAtLeeway));
}

private Duration getOidcBackChannelNotBeforeIssuedAtExpiresAtLeeway()
{
return assertPositive(Duration.parse(oidcBackChannelNotBeforeIssuedAtExpiresAtLeeway));
}

@Bean
@Lazy
public JwtVerifier jwtVerifier()
{
return new JwtVerifierImpl(oidcProviderRealmBaseUrl, oidcClientId, oidcBearerTokenAudience, baseOidcClient());
return new JwtVerifierImpl(oidcProviderRealmBaseUrl, oidcClientId, oidcBearerTokenAudience,
getOidcBearerTokenNotBeforeIssuedAtExpiresAtLeeway(),
getOidcBackChannelNotBeforeIssuedAtExpiresAtLeeway(), baseOidcClient());
}

@Bean
Expand Down Expand Up @@ -510,7 +530,7 @@ private Duration oidcProviderClientTimeoutConnect()

private int oidcSessionTimeout()
{
long seconds = assertPositive(Duration.parse(oidcSessionTimeout)).getSeconds();
long seconds = assertPositive(Duration.parse(oidcSessionTimeout)).toSeconds();

if (seconds >= Integer.MAX_VALUE)
seconds = Integer.MAX_VALUE;
Expand Down
6 changes: 6 additions & 0 deletions dsf-common/dsf-common-oidc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,11 @@
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2018-2025 Heilbronn University of Applied Sciences
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.dsf.common.oidc;

import com.auth0.jwt.interfaces.Verification;

public interface BaseJwtVerifier
{
Verification createVerification(String token) throws OidcClientException;
}
Loading
Loading