use of org.keycloak.models.SingleUseTokenStoreProvider in project keycloak by keycloak.
the class JWTClientAuthenticator method authenticateClient.
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
// KEYCLOAK-19461: Needed for quarkus resteasy implementation throws exception when called with mediaType authentication/json in OpenShiftTokenReviewEndpoint
if (!isFormDataRequest(context.getHttpRequest())) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing");
context.challenge(challengeResponse);
return;
}
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
String clientAssertionType = params.getFirst(OAuth2Constants.CLIENT_ASSERTION_TYPE);
String clientAssertion = params.getFirst(OAuth2Constants.CLIENT_ASSERTION);
if (clientAssertionType == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing");
context.challenge(challengeResponse);
return;
}
if (!clientAssertionType.equals(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type has value '" + clientAssertionType + "' but expected is '" + OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT + "'");
context.challenge(challengeResponse);
return;
}
if (clientAssertion == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "client_assertion parameter missing");
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
return;
}
try {
JWSInput jws = new JWSInput(clientAssertion);
JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
RealmModel realm = context.getRealm();
String clientId = token.getSubject();
if (clientId == null) {
throw new RuntimeException("Can't identify client. Subject missing on JWT token");
}
if (!clientId.equals(token.getIssuer())) {
throw new RuntimeException("Issuer mismatch. The issuer should match the subject");
}
context.getEvent().client(clientId);
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) {
context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
return;
} else {
context.setClient(client);
}
if (!client.isEnabled()) {
context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
return;
}
String expectedSignatureAlg = OIDCAdvancedConfigWrapper.fromClientModel(client).getTokenEndpointAuthSigningAlg();
if (jws.getHeader().getAlgorithm() == null || jws.getHeader().getAlgorithm().name() == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "invalid signature algorithm");
context.challenge(challengeResponse);
return;
}
String actualSignatureAlg = jws.getHeader().getAlgorithm().name();
if (expectedSignatureAlg != null && !expectedSignatureAlg.equals(actualSignatureAlg)) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "invalid signature algorithm");
context.challenge(challengeResponse);
return;
}
// Get client key and validate signature
PublicKey clientPublicKey = getSignatureValidationKey(client, context, jws);
if (clientPublicKey == null) {
// Error response already set to context
return;
}
boolean signatureValid;
try {
JsonWebToken jwt = context.getSession().tokens().decodeClientJWT(clientAssertion, client, JsonWebToken.class);
signatureValid = jwt != null;
} catch (RuntimeException e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
throw new RuntimeException("Signature on JWT token failed validation", cause);
}
if (!signatureValid) {
throw new RuntimeException("Signature on JWT token failed validation");
}
// Allow both "issuer" or "token-endpoint" as audience
List<String> expectedAudiences = getExpectedAudiences(context, realm);
if (!token.hasAnyAudience(expectedAudiences)) {
throw new RuntimeException("Token audience doesn't match domain. Expected audiences are any of " + expectedAudiences + " but audience from token is '" + Arrays.asList(token.getAudience()) + "'");
}
if (!token.isActive()) {
throw new RuntimeException("Token is not active");
}
// KEYCLOAK-2986
int currentTime = Time.currentTime();
if (token.getExpiration() == 0 && token.getIssuedAt() + 10 < currentTime) {
throw new RuntimeException("Token is not active");
}
if (token.getId() == null) {
throw new RuntimeException("Missing ID on the token");
}
SingleUseTokenStoreProvider singleUseCache = context.getSession().getProvider(SingleUseTokenStoreProvider.class);
int lifespanInSecs = Math.max(token.getExpiration() - currentTime, 10);
if (singleUseCache.putIfAbsent(token.getId(), lifespanInSecs)) {
logger.tracef("Added token '%s' to single-use cache. Lifespan: %d seconds, client: %s", token.getId(), lifespanInSecs, clientId);
} else {
logger.warnf("Token '%s' already used when authenticating client '%s'.", token.getId(), clientId);
throw new RuntimeException("Token reuse detected");
}
context.success();
} catch (Exception e) {
ServicesLogger.LOGGER.errorValidatingAssertion(e);
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), OAuthErrorException.INVALID_CLIENT, "Client authentication with signed JWT failed: " + e.getMessage());
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
}
}
use of org.keycloak.models.SingleUseTokenStoreProvider in project keycloak by keycloak.
the class JWTClientSecretAuthenticator method authenticateClient.
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
// KEYCLOAK-19461: Needed for quarkus resteasy implementation throws exception when called with mediaType authentication/json in OpenShiftTokenReviewEndpoint
if (!isFormDataRequest(context.getHttpRequest())) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing");
context.challenge(challengeResponse);
return;
}
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
String clientAssertionType = params.getFirst(OAuth2Constants.CLIENT_ASSERTION_TYPE);
String clientAssertion = params.getFirst(OAuth2Constants.CLIENT_ASSERTION);
if (clientAssertionType == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing");
context.challenge(challengeResponse);
return;
}
if (!clientAssertionType.equals(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type has value '" + clientAssertionType + "' but expected is '" + OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT + "'");
context.challenge(challengeResponse);
return;
}
if (clientAssertion == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "client_assertion parameter missing");
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
return;
}
try {
JWSInput jws = new JWSInput(clientAssertion);
JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
RealmModel realm = context.getRealm();
String clientId = token.getSubject();
if (clientId == null) {
throw new RuntimeException("Can't identify client. Subject missing on JWT token");
}
if (!clientId.equals(token.getIssuer())) {
throw new RuntimeException("Issuer mismatch. The issuer should match the subject");
}
context.getEvent().client(clientId);
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) {
context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
return;
} else {
context.setClient(client);
}
if (!client.isEnabled()) {
context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
return;
}
String expectedSignatureAlg = OIDCAdvancedConfigWrapper.fromClientModel(client).getTokenEndpointAuthSigningAlg();
if (jws.getHeader().getAlgorithm() == null || jws.getHeader().getAlgorithm().name() == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "invalid signature algorithm");
context.challenge(challengeResponse);
return;
}
String actualSignatureAlg = jws.getHeader().getAlgorithm().name();
if (expectedSignatureAlg != null && !expectedSignatureAlg.equals(actualSignatureAlg)) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "invalid signature algorithm");
context.challenge(challengeResponse);
return;
}
String clientSecretString = client.getSecret();
if (clientSecretString == null) {
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, null);
return;
}
boolean signatureValid;
try {
JsonWebToken jwt = context.getSession().tokens().decodeClientJWT(clientAssertion, client, JsonWebToken.class);
signatureValid = jwt != null;
} catch (RuntimeException e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
throw new RuntimeException("Signature on JWT token by client secret failed validation", cause);
}
if (!signatureValid) {
throw new RuntimeException("Signature on JWT token by client secret failed validation");
}
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
// JWT contents and verification in client_secret_jwt is the same as in private_key_jwt
// Allow both "issuer" or "token-endpoint" as audience
String issuerUrl = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
String tokenUrl = OIDCLoginProtocolService.tokenUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
if (!token.hasAudience(issuerUrl) && !token.hasAudience(tokenUrl)) {
throw new RuntimeException("Token audience doesn't match domain. Realm issuer is '" + issuerUrl + "' but audience from token is '" + Arrays.asList(token.getAudience()).toString() + "'");
}
if (!token.isActive()) {
throw new RuntimeException("Token is not active");
}
// KEYCLOAK-2986, token-timeout or token-expiration in keycloak.json might not be used
int currentTime = Time.currentTime();
if (token.getExpiration() == 0 && token.getIssuedAt() + 10 < currentTime) {
throw new RuntimeException("Token is not active");
}
if (token.getId() == null) {
throw new RuntimeException("Missing ID on the token");
}
SingleUseTokenStoreProvider singleUseCache = context.getSession().getProvider(SingleUseTokenStoreProvider.class);
int lifespanInSecs = Math.max(token.getExpiration() - currentTime, 10);
if (singleUseCache.putIfAbsent(token.getId(), lifespanInSecs)) {
logger.tracef("Added token '%s' to single-use cache. Lifespan: %d seconds, client: %s", token.getId(), lifespanInSecs, clientId);
} else {
logger.warnf("Token '%s' already used when authenticating client '%s'.", token.getId(), clientId);
throw new RuntimeException("Token reuse detected");
}
context.success();
} catch (Exception e) {
ServicesLogger.LOGGER.errorValidatingAssertion(e);
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client authentication with client secret signed JWT failed: " + e.getMessage());
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
}
}
Aggregations