/* * 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 org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuthErrorException; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.common.util.Time; import org.keycloak.events.Details; import org.keycloak.jose.jws.Algorithm; import org.keycloak.models.Constants; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.admin.AbstractAdminTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.OAuthGrantPage; import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** * Test for supporting advanced parameters of OIDC specs (max_age, prompt, ...) * * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest { @Rule public AssertEvents events = new AssertEvents(this); @Page protected AppPage appPage; @Page protected LoginPage loginPage; @Page protected AccountUpdateProfilePage profilePage; @Page protected OAuthGrantPage grantPage; @Page protected ErrorPage errorPage; @Override public void configureTestRealm(RealmRepresentation testRealm) { } @Before public void clientConfiguration() { ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true); /* * Configure the default client ID. Seems like OAuthClient is keeping the state of clientID * For example: If some test case configure oauth.clientId("sample-public-client"), other tests * will faile and the clientID will always be "sample-public-client * @see AccessTokenTest#testAuthorizationNegotiateHeaderIgnored() */ oauth.clientId("test-app"); oauth.maxAge(null); } @Override public void addTestRealms(List<RealmRepresentation> testRealms) { RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); testRealms.add(realm); } // Max_age @Test public void testMaxAge1() { // Open login form and login successfully oauth.doLogin("test-user@localhost", "password"); EventRepresentation loginEvent = events.expectLogin().assertEvent(); IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); // Check that authTime is available and set to current time int authTime = idToken.getAuthTime(); int currentTime = Time.currentTime(); Assert.assertTrue(authTime <= currentTime && authTime + 3 >= currentTime); // Set time offset setTimeOffset(10); // Now open login form with maxAge=1 oauth.maxAge("1"); // Assert I need to login again through the login form oauth.doLogin("test-user@localhost", "password"); loginEvent = events.expectLogin().assertEvent(); idToken = sendTokenRequestAndGetIDToken(loginEvent); // Assert that authTime was updated int authTimeUpdated = idToken.getAuthTime(); Assert.assertTrue(authTime + 10 <= authTimeUpdated); } @Test public void testMaxAge10000() { // Open login form and login successfully oauth.doLogin("test-user@localhost", "password"); EventRepresentation loginEvent = events.expectLogin().assertEvent(); IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); // Check that authTime is available and set to current time int authTime = idToken.getAuthTime(); int currentTime = Time.currentTime(); Assert.assertTrue(authTime <= currentTime && authTime + 3 >= currentTime); // Set time offset setTimeOffset(10); // Now open login form with maxAge=10000 oauth.maxAge("10000"); // Assert that I will be automatically logged through cookie oauth.openLoginForm(); loginEvent = events.expectLogin().assertEvent(); idToken = sendTokenRequestAndGetIDToken(loginEvent); // Assert that authTime is still the same int authTimeUpdated = idToken.getAuthTime(); Assert.assertEquals(authTime, authTimeUpdated); } // Prompt @Test public void promptNoneNotLogged() { // Send request with prompt=none driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=none"); assertFalse(loginPage.isCurrent()); assertTrue(appPage.isCurrent()); events.assertEmpty(); // Assert error response was sent because not logged in OAuthClient.AuthorizationEndpointResponse resp = new OAuthClient.AuthorizationEndpointResponse(oauth); Assert.assertNull(resp.getCode()); Assert.assertEquals(OAuthErrorException.LOGIN_REQUIRED, resp.getError()); } @Test public void promptNoneSuccess() { // Login user loginPage.open(); loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); int authTime = idToken.getAuthTime(); // Set time offset setTimeOffset(10); // Assert user still logged with previous authTime driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=none"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); loginEvent = events.expectLogin().removeDetail(Details.USERNAME).assertEvent(); idToken = sendTokenRequestAndGetIDToken(loginEvent); int authTime2 = idToken.getAuthTime(); Assert.assertEquals(authTime, authTime2); } // Prompt=none with consent required for client @Test public void promptNoneConsentRequired() throws Exception { // Require consent ClientManager.realm(adminClient.realm("test")).clientId("test-app").consentRequired(true); try { // login to account mgmt. profilePage.open(); assertTrue(loginPage.isCurrent()); loginPage.login("test-user@localhost", "password"); profilePage.assertCurrent(); events.expectLogin().client(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID) .removeDetail(Details.REDIRECT_URI) .detail(Details.USERNAME, "test-user@localhost").assertEvent(); // Assert error shown when trying prompt=none and consent not yet retrieved driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=none"); assertTrue(appPage.isCurrent()); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); OAuthClient.AuthorizationEndpointResponse resp = new OAuthClient.AuthorizationEndpointResponse(oauth); Assert.assertNull(resp.getCode()); Assert.assertEquals(OAuthErrorException.INTERACTION_REQUIRED, resp.getError()); // Confirm consent driver.navigate().to(oauth.getLoginFormUrl()); grantPage.assertCurrent(); grantPage.accept(); events.expectLogin() .detail(Details.USERNAME, "test-user@localhost") .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED) .assertEvent(); // Consent not required anymore. Login with prompt=none should success driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=none"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); resp = new OAuthClient.AuthorizationEndpointResponse(oauth); Assert.assertNotNull(resp.getCode()); Assert.assertNull(resp.getError()); events.expectLogin() .detail(Details.USERNAME, "test-user@localhost") .detail(Details.CONSENT, Details.CONSENT_VALUE_PERSISTED_CONSENT) .assertEvent(); } finally { // revert require consent ClientManager.realm(adminClient.realm("test")).clientId("test-app").consentRequired(false); } } // prompt=login @Test public void promptLogin() { // Login user loginPage.open(); loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); int authTime = idToken.getAuthTime(); // Set time offset setTimeOffset(10); // Assert need to re-authenticate with prompt=login driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=login"); loginPage.assertCurrent(); loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); idToken = sendTokenRequestAndGetIDToken(loginEvent); int authTimeUpdated = idToken.getAuthTime(); // Assert that authTime was updated Assert.assertTrue(authTime + 10 <= authTimeUpdated); } @Test public void promptLoginDifferentUser() throws Exception { String sss = oauth.getLoginFormUrl(); System.out.println(sss); // Login user loginPage.open(); loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); // Assert need to re-authenticate with prompt=login driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=login"); // Authenticate as different user loginPage.assertCurrent(); loginPage.login("john-doh@localhost", "password"); errorPage.assertCurrent(); Assert.assertTrue(errorPage.getError().startsWith("You are already authenticated as different user")); } // DISPLAY & OTHERS @Test public void nonSupportedParams() { driver.navigate().to(oauth.getLoginFormUrl() + "&display=popup&foo=foobar&claims_locales=fr"); loginPage.assertCurrent(); loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); Assert.assertNotNull(idToken); } // REQUEST & REQUEST_URI @Test public void requestParamUnsigned() throws Exception { oauth.stateParamHardcoded("mystate2"); String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); // Send request object with invalid redirect uri. oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString()); String requestStr = oidcClientEndpointsResource.getOIDCRequest(); oauth.request(requestStr); oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); // Assert the value from request object has bigger priority then from the query parameter. oauth.redirectUri("http://invalid"); oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); requestStr = oidcClientEndpointsResource.getOIDCRequest(); oauth.request(requestStr); OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertNotNull(response.getCode()); Assert.assertEquals("mystate2", response.getState()); assertTrue(appPage.isCurrent()); } @Test public void requestUriParamUnsigned() throws Exception { oauth.stateParamHardcoded("mystate1"); String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); // Send request object with invalid redirect uri. oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString()); oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); // Assert the value from request object has bigger priority then from the query parameter. oauth.redirectUri("http://invalid"); oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertNotNull(response.getCode()); Assert.assertEquals("mystate1", response.getState()); assertTrue(appPage.isCurrent()); } @Test public void requestUriParamSigned() throws Exception { oauth.stateParamHardcoded("mystate3"); String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); // Set required signature for request_uri ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); ClientRepresentation clientRep = clientResource.toRepresentation(); OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(Algorithm.RS256); clientResource.update(clientRep); // Verify unsigned request_uri will fail oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); assertEquals("Invalid Request", errorPage.getError()); // Generate keypair for client String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys().get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY); // Verify signed request_uri will fail due to failed signature validation oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.RS256.toString()); oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); assertEquals("Invalid Request", errorPage.getError()); // Update clientModel with publicKey for signing clientRep = clientResource.toRepresentation(); CertificateRepresentation cert = new CertificateRepresentation(); cert.setPublicKey(clientPublicKeyPem); CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, cert, JWTClientAuthenticator.ATTR_PREFIX); clientResource.update(clientRep); // set time offset, so that new keys are downloaded setTimeOffset(20); // Check signed request_uri will pass OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertNotNull(response.getCode()); Assert.assertEquals("mystate3", response.getState()); assertTrue(appPage.isCurrent()); // Revert requiring signature for client OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(null); clientResource.update(clientRep); } // LOGIN_HINT @Test public void loginHint() { // Assert need to re-authenticate with prompt=login driver.navigate().to(oauth.getLoginFormUrl() + "&" + OIDCLoginProtocol.LOGIN_HINT_PARAM + "=test-user%40localhost"); loginPage.assertCurrent(); Assert.assertEquals("test-user@localhost", loginPage.getUsername()); loginPage.login("password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); } }