Search in sources :

Example 1 with SingleUseTokenStoreProvider

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);
    }
}
Also used : PublicKey(java.security.PublicKey) JWSInput(org.keycloak.jose.jws.JWSInput) JsonWebToken(org.keycloak.representations.JsonWebToken) ParEndpoint(org.keycloak.protocol.oidc.par.endpoints.ParEndpoint) OAuthErrorException(org.keycloak.OAuthErrorException) Response(javax.ws.rs.core.Response) RealmModel(org.keycloak.models.RealmModel) ClientModel(org.keycloak.models.ClientModel) SingleUseTokenStoreProvider(org.keycloak.models.SingleUseTokenStoreProvider)

Example 2 with SingleUseTokenStoreProvider

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);
    }
}
Also used : JWSInput(org.keycloak.jose.jws.JWSInput) JsonWebToken(org.keycloak.representations.JsonWebToken) Response(javax.ws.rs.core.Response) RealmModel(org.keycloak.models.RealmModel) ClientModel(org.keycloak.models.ClientModel) SingleUseTokenStoreProvider(org.keycloak.models.SingleUseTokenStoreProvider)

Aggregations

Response (javax.ws.rs.core.Response)2 JWSInput (org.keycloak.jose.jws.JWSInput)2 ClientModel (org.keycloak.models.ClientModel)2 RealmModel (org.keycloak.models.RealmModel)2 SingleUseTokenStoreProvider (org.keycloak.models.SingleUseTokenStoreProvider)2 JsonWebToken (org.keycloak.representations.JsonWebToken)2 PublicKey (java.security.PublicKey)1 OAuthErrorException (org.keycloak.OAuthErrorException)1 ParEndpoint (org.keycloak.protocol.oidc.par.endpoints.ParEndpoint)1