Search in sources :

Example 31 with JsonWebToken

use of org.keycloak.representations.JsonWebToken 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 32 with JsonWebToken

use of org.keycloak.representations.JsonWebToken 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)

Example 33 with JsonWebToken

use of org.keycloak.representations.JsonWebToken in project keycloak by keycloak.

the class ClientAuthSignedJWTTest method testEndpointAsAudience.

private void testEndpointAsAudience(String endpointUrl) throws Exception {
    ClientRepresentation clientRepresentation = app2;
    ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
    clientRepresentation = clientResource.toRepresentation();
    try {
        KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource);
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl());
        assertion.audience(endpointUrl);
        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, assertion)));
        try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) {
            OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
            assertNotNull(response.getAccessToken());
        }
    } finally {
        revertJwksUriSettings(clientRepresentation, clientResource);
    }
}
Also used : NameValuePair(org.apache.http.NameValuePair) BasicNameValuePair(org.apache.http.message.BasicNameValuePair) KeyPair(java.security.KeyPair) PrivateKey(java.security.PrivateKey) OAuthClient(org.keycloak.testsuite.util.OAuthClient) PublicKey(java.security.PublicKey) JsonWebToken(org.keycloak.representations.JsonWebToken) LinkedList(java.util.LinkedList) ClientRepresentation(org.keycloak.representations.idm.ClientRepresentation) BasicNameValuePair(org.apache.http.message.BasicNameValuePair) CloseableHttpResponse(org.apache.http.client.methods.CloseableHttpResponse) ClientResource(org.keycloak.admin.client.resource.ClientResource)

Example 34 with JsonWebToken

use of org.keycloak.representations.JsonWebToken in project keycloak by keycloak.

the class ClientAuthSecretSignedJWTTest method testInvalidIssuer.

@Test
public void testInvalidIssuer() throws Exception {
    oauth.clientId("test-app");
    oauth.doLogin("test-user@localhost", "password");
    String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
    JWTClientSecretCredentialsProvider jwtProvider = new JWTClientSecretCredentialsProvider() {

        @Override
        protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
            JsonWebToken jwt = super.createRequestToken(clientId, realmInfoUrl);
            jwt.issuer("bad-issuer");
            return jwt;
        }
    };
    String algorithm = Algorithm.HS256;
    jwtProvider.setClientSecret("password", algorithm);
    String jwt = jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl(), algorithm);
    OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, jwt);
    assertEquals(400, response.getStatusCode());
    assertEquals("invalid_client", response.getError());
}
Also used : JWTClientSecretCredentialsProvider(org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider) OAuthClient(org.keycloak.testsuite.util.OAuthClient) JsonWebToken(org.keycloak.representations.JsonWebToken) AbstractAdminTest(org.keycloak.testsuite.admin.AbstractAdminTest) Test(org.junit.Test) AbstractKeycloakTest(org.keycloak.testsuite.AbstractKeycloakTest)

Example 35 with JsonWebToken

use of org.keycloak.representations.JsonWebToken in project keycloak by keycloak.

the class ClientAuthSignedJWTTest method testInvalidAudience.

@Test
public void testInvalidAudience() throws Exception {
    ClientRepresentation clientRepresentation = app2;
    ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
    clientRepresentation = clientResource.toRepresentation();
    try {
        KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource);
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl());
        assertion.audience("https://as.other.org");
        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, assertion)));
        try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) {
            OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
            assertNull(response.getAccessToken());
        }
    } finally {
        revertJwksUriSettings(clientRepresentation, clientResource);
    }
}
Also used : NameValuePair(org.apache.http.NameValuePair) BasicNameValuePair(org.apache.http.message.BasicNameValuePair) KeyPair(java.security.KeyPair) PrivateKey(java.security.PrivateKey) OAuthClient(org.keycloak.testsuite.util.OAuthClient) PublicKey(java.security.PublicKey) JsonWebToken(org.keycloak.representations.JsonWebToken) LinkedList(java.util.LinkedList) ClientRepresentation(org.keycloak.representations.idm.ClientRepresentation) BasicNameValuePair(org.apache.http.message.BasicNameValuePair) CloseableHttpResponse(org.apache.http.client.methods.CloseableHttpResponse) ClientResource(org.keycloak.admin.client.resource.ClientResource) AbstractKeycloakTest(org.keycloak.testsuite.AbstractKeycloakTest) Test(org.junit.Test)

Aggregations

JsonWebToken (org.keycloak.representations.JsonWebToken)36 Test (org.junit.Test)12 JWSInput (org.keycloak.jose.jws.JWSInput)7 JWSBuilder (org.keycloak.jose.jws.JWSBuilder)5 KeyPair (java.security.KeyPair)4 IdentityBrokerException (org.keycloak.broker.provider.IdentityBrokerException)4 ClientRepresentation (org.keycloak.representations.idm.ClientRepresentation)4 OAuthClient (org.keycloak.testsuite.util.OAuthClient)4 PublicKey (java.security.PublicKey)3 OAuthErrorException (org.keycloak.OAuthErrorException)3 JWSInputException (org.keycloak.jose.jws.JWSInputException)3 IOException (java.io.IOException)2 PrivateKey (java.security.PrivateKey)2 LinkedList (java.util.LinkedList)2 Response (javax.ws.rs.core.Response)2 NameValuePair (org.apache.http.NameValuePair)2 CloseableHttpResponse (org.apache.http.client.methods.CloseableHttpResponse)2 BasicNameValuePair (org.apache.http.message.BasicNameValuePair)2 ClientResource (org.keycloak.admin.client.resource.ClientResource)2 BrokeredIdentityContext (org.keycloak.broker.provider.BrokeredIdentityContext)2