Merge pull request #3032 from mposolda/KEYCLOAK-3223

KEYCLOAK-3223 Basic support for acr claim
This commit is contained in:
Marek Posolda 2016-07-14 16:26:36 +02:00 committed by GitHub
commit bffbc9e198
7 changed files with 79 additions and 33 deletions

View file

@ -47,6 +47,8 @@ public class IDToken extends JsonWebToken {
public static final String ADDRESS = "address"; public static final String ADDRESS = "address";
public static final String UPDATED_AT = "updated_at"; public static final String UPDATED_AT = "updated_at";
public static final String CLAIMS_LOCALES = "claims_locales"; public static final String CLAIMS_LOCALES = "claims_locales";
public static final String ACR = "acr";
// NOTE!!! WE used to use @JsonUnwrapped on a UserClaimSet object. This screws up otherClaims and the won't work // NOTE!!! WE used to use @JsonUnwrapped on a UserClaimSet object. This screws up otherClaims and the won't work
// anymore. So don't have any @JsonUnwrapped! // anymore. So don't have any @JsonUnwrapped!
@JsonProperty(NONCE) @JsonProperty(NONCE)
@ -118,6 +120,9 @@ public class IDToken extends JsonWebToken {
@JsonProperty(CLAIMS_LOCALES) @JsonProperty(CLAIMS_LOCALES)
protected String claimsLocales; protected String claimsLocales;
@JsonProperty(ACR)
protected String acr;
public String getNonce() { public String getNonce() {
return nonce; return nonce;
} }
@ -302,4 +307,11 @@ public class IDToken extends JsonWebToken {
this.claimsLocales = claimsLocales; this.claimsLocales = claimsLocales;
} }
public String getAcr() {
return acr;
}
public void setAcr(String acr) {
this.acr = acr;
}
} }

View file

@ -54,7 +54,7 @@ public class CookieAuthenticator implements Authenticator {
context.attempted(); context.attempted();
} else { } else {
ClientSessionModel clientSession = context.getClientSession(); ClientSessionModel clientSession = context.getClientSession();
clientSession.setNote(AuthenticationManager.SKIP_AUTH_TIME_UPDATE, "true"); clientSession.setNote(AuthenticationManager.SSO_AUTH, "true");
context.setUser(authResult.getUser()); context.setUser(authResult.getUser());
context.attachUserSession(authResult.getSession()); context.attachUserSession(authResult.getSession());

View file

@ -524,6 +524,11 @@ public class TokenManager {
token.issuer(clientSession.getNote(OIDCLoginProtocol.ISSUER)); token.issuer(clientSession.getNote(OIDCLoginProtocol.ISSUER));
token.setNonce(clientSession.getNote(OIDCLoginProtocol.NONCE_PARAM)); token.setNonce(clientSession.getNote(OIDCLoginProtocol.NONCE_PARAM));
// Best effort for "acr" value. Use 0 if clientSession was authenticated through cookie ( SSO )
// TODO: Add better acr support. See KEYCLOAK-3314
String acr = (AuthenticationManager.isSSOAuthentication(clientSession)) ? "0" : "1";
token.setAcr(acr);
String authTime = session.getNote(AuthenticationManager.AUTH_TIME); String authTime = session.getNote(AuthenticationManager.AUTH_TIME);
if (authTime != null) { if (authTime != null) {
token.setAuthTime(Integer.parseInt(authTime)); token.setAuthTime(Integer.parseInt(authTime));
@ -673,6 +678,7 @@ public class TokenManager {
idToken.setAuthTime(accessToken.getAuthTime()); idToken.setAuthTime(accessToken.getAuthTime());
idToken.setSessionState(accessToken.getSessionState()); idToken.setSessionState(accessToken.getSessionState());
idToken.expiration(accessToken.getExpiration()); idToken.expiration(accessToken.getExpiration());
idToken.setAcr(accessToken.getAcr());
transformIDToken(session, idToken, realm, client, userSession.getUser(), userSession, clientSession); transformIDToken(session, idToken, realm, client, userSession.getUser(), userSession, clientSession);
return this; return this;
} }

View file

@ -64,8 +64,8 @@ public class AuthenticationManager {
// userSession note with authTime (time when authentication flow including requiredActions was finished) // userSession note with authTime (time when authentication flow including requiredActions was finished)
public static final String AUTH_TIME = "AUTH_TIME"; public static final String AUTH_TIME = "AUTH_TIME";
// clientSession note with flag that authTime update should be skipped // clientSession note with flag that clientSession was authenticated through SSO cookie
public static final String SKIP_AUTH_TIME_UPDATE = "SKIP_AUTH_TIME_UPDATE"; public static final String SSO_AUTH = "SSO_AUTH";
protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER; protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
public static final String FORM_USERNAME = "username"; public static final String FORM_USERNAME = "username";
@ -410,9 +410,8 @@ public class AuthenticationManager {
if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN); if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN);
if (userSession.isRememberMe()) createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection); if (userSession.isRememberMe()) createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection);
// Update userSession note with authTime. But just if flag SKIP_AUTH_TIME_UPDATE is not set // Update userSession note with authTime. But just if flag SSO_AUTH is not set
String skipAuthTimeUpdate = clientSession.getNote(SKIP_AUTH_TIME_UPDATE); if (!isSSOAuthentication(clientSession)) {
if (skipAuthTimeUpdate == null || !Boolean.parseBoolean(skipAuthTimeUpdate)) {
int authTime = Time.currentTime(); int authTime = Time.currentTime();
userSession.setNote(AUTH_TIME, String.valueOf(authTime)); userSession.setNote(AUTH_TIME, String.valueOf(authTime));
} }
@ -420,6 +419,13 @@ public class AuthenticationManager {
return protocol.authenticated(userSession, new ClientSessionCode(realm, clientSession)); return protocol.authenticated(userSession, new ClientSessionCode(realm, clientSession));
} }
public static boolean isSSOAuthentication(ClientSessionModel clientSession) {
String ssoAuth = clientSession.getNote(SSO_AUTH);
return Boolean.parseBoolean(ssoAuth);
}
public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession,
ClientConnection clientConnection, ClientConnection clientConnection,
HttpRequest request, UriInfo uriInfo, EventBuilder event) { HttpRequest request, UriInfo uriInfo, EventBuilder event) {

View file

@ -17,12 +17,19 @@
package org.keycloak.testsuite; package org.keycloak.testsuite;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.reflections.Reflections;
import org.keycloak.events.Details;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.util.OAuthClient;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
@ -74,4 +81,23 @@ public abstract class TestRealmKeycloakTest extends AbstractKeycloakTest {
*/ */
public abstract void configureTestRealm(RealmRepresentation testRealm); public abstract void configureTestRealm(RealmRepresentation testRealm);
protected IDToken sendTokenRequestAndGetIDToken(EventRepresentation loginEvent) {
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
Assert.assertEquals(200, response.getStatusCode());
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
Field eventsField = Reflections.findDeclaredField(this.getClass(), "events");
if (eventsField != null) {
AssertEvents events = Reflections.getFieldValue(eventsField, this, AssertEvents.class);
events.expectCodeToToken(codeId, sessionId).assertEvent();
}
return idToken;
}
} }

View file

@ -23,6 +23,7 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
@ -73,7 +74,11 @@ public class SSOTest extends TestRealmKeycloakTest {
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
String sessionId = events.expectLogin().assertEvent().getSessionId(); EventRepresentation loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent);
Assert.assertEquals("1", idToken.getAcr());
appPage.open(); appPage.open();
@ -81,14 +86,18 @@ public class SSOTest extends TestRealmKeycloakTest {
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
profilePage.open(); loginEvent = events.expectLogin().removeDetail(Details.USERNAME).client("test-app").assertEvent();
String sessionId2 = loginEvent.getSessionId();
assertTrue(profilePage.isCurrent());
String sessionId2 = events.expectLogin().removeDetail(Details.USERNAME).client("test-app").assertEvent().getSessionId();
assertEquals(sessionId, sessionId2); assertEquals(sessionId, sessionId2);
// acr is 0 as we authenticated through SSO cookie
idToken = sendTokenRequestAndGetIDToken(loginEvent);
Assert.assertEquals("0", idToken.getAcr());
profilePage.open();
assertTrue(profilePage.isCurrent());
// Expire session // Expire session
testingClient.testing().removeUserSession("test", sessionId); testingClient.testing().removeUserSession("test", sessionId);

View file

@ -31,24 +31,25 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.TestRealmKeycloakTest;
import org.keycloak.testsuite.admin.AbstractAdminTest; import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
/** /**
* Test for supporting advanced parameters of OIDC specs (max_age, nonce, prompt, ...) * Test for supporting advanced parameters of OIDC specs (max_age, nonce, prompt, ...)
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class OIDCAdvancedRequestParamsTest extends AbstractKeycloakTest { public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest {
@Rule @Rule
public AssertEvents events = new AssertEvents(this); public AssertEvents events = new AssertEvents(this);
@Override @Override
public void beforeAbstractKeycloakTest() throws Exception { public void configureTestRealm(RealmRepresentation testRealm) {
super.beforeAbstractKeycloakTest();
} }
@Before @Before
@ -76,7 +77,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractKeycloakTest {
oauth.doLogin("test-user@localhost", "password"); oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent(); EventRepresentation loginEvent = events.expectLogin().assertEvent();
IDToken idToken = retrieveIDToken(loginEvent); IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent);
// Check that authTime is available and set to current time // Check that authTime is available and set to current time
int authTime = idToken.getAuthTime(); int authTime = idToken.getAuthTime();
@ -93,7 +94,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractKeycloakTest {
oauth.doLogin("test-user@localhost", "password"); oauth.doLogin("test-user@localhost", "password");
loginEvent = events.expectLogin().assertEvent(); loginEvent = events.expectLogin().assertEvent();
idToken = retrieveIDToken(loginEvent); idToken = sendTokenRequestAndGetIDToken(loginEvent);
// Assert that authTime was updated // Assert that authTime was updated
int authTimeUpdated = idToken.getAuthTime(); int authTimeUpdated = idToken.getAuthTime();
@ -106,7 +107,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractKeycloakTest {
oauth.doLogin("test-user@localhost", "password"); oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent(); EventRepresentation loginEvent = events.expectLogin().assertEvent();
IDToken idToken = retrieveIDToken(loginEvent); IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent);
// Check that authTime is available and set to current time // Check that authTime is available and set to current time
int authTime = idToken.getAuthTime(); int authTime = idToken.getAuthTime();
@ -123,25 +124,11 @@ public class OIDCAdvancedRequestParamsTest extends AbstractKeycloakTest {
oauth.openLoginForm(); oauth.openLoginForm();
loginEvent = events.expectLogin().assertEvent(); loginEvent = events.expectLogin().assertEvent();
idToken = retrieveIDToken(loginEvent); idToken = sendTokenRequestAndGetIDToken(loginEvent);
// Assert that authTime is still the same // Assert that authTime is still the same
int authTimeUpdated = idToken.getAuthTime(); int authTimeUpdated = idToken.getAuthTime();
Assert.assertEquals(authTime, authTimeUpdated); Assert.assertEquals(authTime, authTimeUpdated);
} }
private IDToken retrieveIDToken(EventRepresentation loginEvent) {
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
Assert.assertEquals(200, response.getStatusCode());
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
events.expectCodeToToken(codeId, sessionId).assertEvent();
return idToken;
}
} }