Search in sources :

Example 51 with JWSInput

use of org.keycloak.jose.jws.JWSInput in project keycloak by keycloak.

the class FixedHostnameTest method assertTokenIssuer.

private void assertTokenIssuer(String realm, String expectedBaseUrl) throws Exception {
    oauth.realm(realm);
    OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
    AccessToken token = new JWSInput(tokenResponse.getAccessToken()).readJsonContent(AccessToken.class);
    assertEquals(expectedBaseUrl + "/auth/realms/" + realm, token.getIssuer());
    String introspection = oauth.introspectAccessTokenWithClientCredential(oauth.getClientId(), "password", tokenResponse.getAccessToken());
    ObjectMapper objectMapper = new ObjectMapper();
    JsonNode introspectionNode = objectMapper.readTree(introspection);
    assertTrue(introspectionNode.get("active").asBoolean());
    assertEquals(expectedBaseUrl + "/auth/realms/" + realm, introspectionNode.get("iss").asText());
}
Also used : OAuthClient(org.keycloak.testsuite.util.OAuthClient) AccessToken(org.keycloak.representations.AccessToken) JsonNode(com.fasterxml.jackson.databind.JsonNode) JWSInput(org.keycloak.jose.jws.JWSInput) ObjectMapper(com.fasterxml.jackson.databind.ObjectMapper)

Example 52 with JWSInput

use of org.keycloak.jose.jws.JWSInput 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 53 with JWSInput

use of org.keycloak.jose.jws.JWSInput 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 54 with JWSInput

use of org.keycloak.jose.jws.JWSInput in project keycloak by keycloak.

the class AuthzClientCredentialsTest method testSuccessfulAuthorizationRequest.

@Test
public void testSuccessfulAuthorizationRequest() throws Exception {
    AuthzClient authzClient = getAuthzClient("keycloak-with-jwt-authentication.json");
    ProtectionResource protection = authzClient.protection();
    PermissionRequest request = new PermissionRequest("Default Resource");
    PermissionResponse ticketResponse = protection.permission().create(request);
    String ticket = ticketResponse.getTicket();
    AuthorizationResponse authorizationResponse = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
    String rpt = authorizationResponse.getToken();
    assertNotNull(rpt);
    AccessToken accessToken = new JWSInput(rpt).readJsonContent(AccessToken.class);
    AccessToken.Authorization authorization = accessToken.getAuthorization();
    assertNotNull(authorization);
    List<Permission> permissions = new ArrayList<>(authorization.getPermissions());
    assertFalse(permissions.isEmpty());
    assertEquals("Default Resource", permissions.get(0).getResourceName());
}
Also used : PermissionRequest(org.keycloak.representations.idm.authorization.PermissionRequest) ProtectionResource(org.keycloak.authorization.client.resource.ProtectionResource) AuthorizationRequest(org.keycloak.representations.idm.authorization.AuthorizationRequest) ArrayList(java.util.ArrayList) PermissionResponse(org.keycloak.representations.idm.authorization.PermissionResponse) JWSInput(org.keycloak.jose.jws.JWSInput) AuthorizationResponse(org.keycloak.representations.idm.authorization.AuthorizationResponse) AuthzClient(org.keycloak.authorization.client.AuthzClient) AccessToken(org.keycloak.representations.AccessToken) Permission(org.keycloak.representations.idm.authorization.Permission) Test(org.junit.Test)

Example 55 with JWSInput

use of org.keycloak.jose.jws.JWSInput in project keycloak by keycloak.

the class HoKTest method expectSuccessfulResponseFromTokenEndpoint.

private void expectSuccessfulResponseFromTokenEndpoint(String sessionId, String codeId, AccessTokenResponse response) throws Exception {
    assertEquals(200, response.getStatusCode());
    Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
    Assert.assertThat(response.getRefreshExpiresIn(), allOf(greaterThanOrEqualTo(1750), lessThanOrEqualTo(1800)));
    assertEquals("Bearer", response.getTokenType());
    String expectedKid = oauth.doCertsRequest("test").getKeys()[0].getKeyId();
    JWSHeader header = new JWSInput(response.getAccessToken()).getHeader();
    assertEquals("RS256", header.getAlgorithm().name());
    assertEquals("JWT", header.getType());
    assertEquals(expectedKid, header.getKeyId());
    assertNull(header.getContentType());
    header = new JWSInput(response.getIdToken()).getHeader();
    assertEquals("RS256", header.getAlgorithm().name());
    assertEquals("JWT", header.getType());
    assertEquals(expectedKid, header.getKeyId());
    assertNull(header.getContentType());
    header = new JWSInput(response.getRefreshToken()).getHeader();
    assertEquals("HS256", header.getAlgorithm().name());
    assertEquals("JWT", header.getType());
    assertNull(header.getContentType());
    AccessToken token = oauth.verifyToken(response.getAccessToken());
    assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject());
    Assert.assertNotEquals("test-user@localhost", token.getSubject());
    assertEquals(sessionId, token.getSessionState());
    assertEquals(2, token.getRealmAccess().getRoles().size());
    assertTrue(token.getRealmAccess().isUserInRole("user"));
    assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size());
    assertTrue(token.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
    EventRepresentation event = events.expectCodeToToken(codeId, sessionId).assertEvent();
    assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
    assertEquals(oauth.parseRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
    assertEquals(sessionId, token.getSessionState());
}
Also used : AccessToken(org.keycloak.representations.AccessToken) EventRepresentation(org.keycloak.representations.idm.EventRepresentation) JWSInput(org.keycloak.jose.jws.JWSInput) JWSHeader(org.keycloak.jose.jws.JWSHeader)

Aggregations

JWSInput (org.keycloak.jose.jws.JWSInput)62 AccessToken (org.keycloak.representations.AccessToken)29 OAuthClient (org.keycloak.testsuite.util.OAuthClient)20 JWSInputException (org.keycloak.jose.jws.JWSInputException)16 Test (org.junit.Test)15 JWSHeader (org.keycloak.jose.jws.JWSHeader)11 Response (javax.ws.rs.core.Response)10 RefreshToken (org.keycloak.representations.RefreshToken)10 EventRepresentation (org.keycloak.representations.idm.EventRepresentation)9 ClientRepresentation (org.keycloak.representations.idm.ClientRepresentation)8 IOException (java.io.IOException)7 VerificationException (org.keycloak.common.VerificationException)7 JsonWebToken (org.keycloak.representations.JsonWebToken)7 JsonNode (com.fasterxml.jackson.databind.JsonNode)5 PublicKey (java.security.PublicKey)5 AccessTokenResponse (org.keycloak.representations.AccessTokenResponse)5 Client (javax.ws.rs.client.Client)4 IDToken (org.keycloak.representations.IDToken)4 ObjectMapper (com.fasterxml.jackson.databind.ObjectMapper)3 List (java.util.List)3