Merge pull request #3014 from mposolda/KEYCLOAK-3222

OIDC client auth fixes & tests
This commit is contained in:
Marek Posolda 2016-07-08 18:16:43 +02:00 committed by GitHub
commit 8bdfd57e9b
11 changed files with 271 additions and 7 deletions

View file

@ -54,6 +54,10 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
this.tokenTimeout = tokenTimeout;
}
protected int getTokenTimeout() {
return tokenTimeout;
}
@Override
public void init(KeycloakDeployment deployment, Object config) {
if (config == null || !(config instanceof Map)) {

View file

@ -26,6 +26,8 @@ public interface OAuth2Constants {
String CLIENT_ID = "client_id";
String CLIENT_SECRET = "client_secret";
String ERROR = "error";
String ERROR_DESCRIPTION = "error_description";

View file

@ -86,7 +86,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
if (formData != null && client_id == null) {
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
clientSecret = formData.getFirst("client_secret");
clientSecret = formData.getFirst(OAuth2Constants.CLIENT_SECRET);
}
if (client_id == null) {

View file

@ -32,6 +32,7 @@ import javax.ws.rs.core.Response;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.AuthenticationExecutionModel;
@ -145,6 +146,11 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
throw new RuntimeException("Token is not active");
}
// KEYCLOAK-2986
if (token.getExpiration() == 0 && token.getIssuedAt() + 10 < Time.currentTime()) {
throw new RuntimeException("Token is not active");
}
context.success();
} catch (Exception e) {
logger.errorValidatingAssertion(e);

View file

@ -18,6 +18,7 @@
package org.keycloak.protocol.oidc;
import org.keycloak.OAuth2Constants;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
@ -39,7 +40,7 @@ import java.util.List;
*/
public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256");
public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
@ -49,6 +50,11 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List<String> DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post");
// Should be rather retrieved dynamically based on available ClientAuthenticator providers?
public static final List<String> DEFAULT_CLIENT_AUTH_METHODS_SUPPORTED = list("client_secret_basic", "client_secret_post", "private_key_jwt");
public static final List<String> DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
private KeycloakSession session;
public OIDCWellKnownProvider(KeycloakSession session) {
@ -78,6 +84,9 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
config.setGrantTypesSupported(DEFAULT_GRANT_TYPES_SUPPORTED);
config.setTokenEndpointAuthMethodsSupported(DEFAULT_CLIENT_AUTH_METHODS_SUPPORTED);
config.setTokenEndpointAuthSigningAlgValuesSupported(DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED);
return config;
}

View file

@ -28,6 +28,8 @@ import org.keycloak.wellknown.WellKnownProviderFactory;
*/
public class OIDCWellKnownProviderFactory implements WellKnownProviderFactory {
public static final String PROVIDER_ID = "openid-configuration";
@Override
public WellKnownProvider create(KeycloakSession session) {
return new OIDCWellKnownProvider(session);
@ -47,7 +49,7 @@ public class OIDCWellKnownProviderFactory implements WellKnownProviderFactory {
@Override
public String getId() {
return "openid-configuration";
return PROVIDER_ID;
}
}

View file

@ -70,6 +70,12 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("registration_endpoint")
private String registrationEndpoint;
@JsonProperty("token_endpoint_auth_methods_supported")
private List<String> tokenEndpointAuthMethodsSupported;
@JsonProperty("token_endpoint_auth_signing_alg_values_supported")
private List<String> tokenEndpointAuthSigningAlgValuesSupported;
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
public String getIssuer() {
@ -176,6 +182,22 @@ public class OIDCConfigurationRepresentation {
this.registrationEndpoint = registrationEndpoint;
}
public List<String> getTokenEndpointAuthMethodsSupported() {
return tokenEndpointAuthMethodsSupported;
}
public void setTokenEndpointAuthMethodsSupported(List<String> tokenEndpointAuthMethodsSupported) {
this.tokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported;
}
public List<String> getTokenEndpointAuthSigningAlgValuesSupported() {
return tokenEndpointAuthSigningAlgValuesSupported;
}
public void setTokenEndpointAuthSigningAlgValuesSupported(List<String> tokenEndpointAuthSigningAlgValuesSupported) {
this.tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported;
}
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;

View file

@ -99,6 +99,10 @@ public class RealmsResource {
return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getBrokerService");
}
public static UriBuilder wellKnownProviderUrl(UriBuilder builder) {
return builder.path(RealmsResource.class).path(RealmsResource.class, "getWellKnown");
}
@Path("{realm}/protocol/{protocol}")
public Object getProtocol(final @PathParam("realm") String name,
final @PathParam("protocol") String protocol) {

View file

@ -0,0 +1,126 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.testsuite.oauth;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.util.OAuthClient;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertEquals;
/**
* Test for "client_secret_post" client authentication (clientID + clientSecret sent in the POST body instead of in "Authorization: Basic" header)
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientAuthPostMethodTest extends AbstractKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
testRealms.add(realm);
}
@Test
public void testPostAuthentication() {
oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = doAccessTokenRequestPostAuth(code, "password");
assertEquals(200, response.getStatusCode());
Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
Assert.assertThat(response.getRefreshExpiresIn(), allOf(greaterThanOrEqualTo(1750), lessThanOrEqualTo(1800)));
AccessToken token = oauth.verifyToken(response.getAccessToken());
EventRepresentation event = events.expectCodeToToken(codeId, sessionId).assertEvent();
assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
assertEquals(sessionId, token.getSessionState());
}
private OAuthClient.AccessTokenResponse doAccessTokenRequestPostAuth(String code, String clientSecret) {
CloseableHttpClient client = new DefaultHttpClient();
try {
HttpPost post = new HttpPost(oauth.getAccessTokenUrl());
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, clientSecret));
UrlEncodedFormEntity formEntity;
try {
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
post.setEntity(formEntity);
try {
return new OAuthClient.AccessTokenResponse(client.execute(post));
} catch (Exception e) {
throw new RuntimeException("Failed to retrieve access token", e);
}
} finally {
oauth.closeClient(client);
}
}
}

View file

@ -613,7 +613,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
}
@Test
@Ignore // Waiting for KEYCLOAK-2986 to be implemented
// KEYCLOAK-2986
public void testMissingExpirationClaim() throws Exception {
// Missing only exp; the lifespan should be calculated from issuedAt
OAuthClient.AccessTokenResponse response = testMissingClaim("expiration");
@ -840,9 +840,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
int now = Time.currentTime();
if (isClaimEnabled("issuedAt")) reqToken.issuedAt(now);
// For the time being there's no getter for tokenTimeout in JWTClientCredentialsProvider
// This is fine because KC doesn't care when exp claim is missing (see KEYCLOAK-2986)
/*if (isClaimEnabled("expiration")) reqToken.expiration(now + getTokenTimeout());*/
if (isClaimEnabled("expiration")) reqToken.expiration(now + getTokenTimeout());
if (isClaimEnabled("notBefore")) reqToken.notBefore(now);
return reqToken;

View file

@ -0,0 +1,91 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.testsuite.oidc;
import java.net.URI;
import java.util.List;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.util.OAuthClient;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
testRealms.add(realm);
}
@Test
public void testDiscovery() {
Client client = ClientBuilder.newClient();
try {
OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryConfiguration(client);
// Support standard + implicit + hybrid flow
assertContains(oidcConfig.getResponseTypesSupported(), OAuth2Constants.CODE, OIDCResponseType.ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT);
assertContains(oidcConfig.getResponseModesSupported(), "query", "fragment");
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "public");
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256.toString());
// Client authentication
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt");
Assert.assertNames(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported(), Algorithm.RS256.toString());
System.out.println("Fopo");
} finally {
client.close();
}
}
private OIDCConfigurationRepresentation getOIDCDiscoveryConfiguration(Client client) {
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
URI oidcDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder).build("test", OIDCWellKnownProviderFactory.PROVIDER_ID);
WebTarget oidcDiscoveryTarget = client.target(oidcDiscoveryUri);
Response response = oidcDiscoveryTarget.request().get();
return response.readEntity(OIDCConfigurationRepresentation.class);
}
private void assertContains(List<String> actual, String... expected) {
for (String exp : expected) {
Assert.assertTrue(actual.contains(exp));
}
}
}