Merge pull request #3032 from mposolda/KEYCLOAK-3223
KEYCLOAK-3223 Basic support for acr claim
This commit is contained in:
commit
bffbc9e198
7 changed files with 79 additions and 33 deletions
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue