ACR support in the javascript adapter

Closes #10154
This commit is contained in:
mposolda 2022-02-09 11:50:37 +01:00 committed by Marek Posolda
parent 6bce8b80b9
commit 52712d2c82
4 changed files with 105 additions and 5 deletions

View file

@ -39,6 +39,19 @@ export interface KeycloakConfig {
clientId: string; clientId: string;
} }
export interface Acr {
/**
* Array of values, which will be used inside ID Token `acr` claim sent inside the `claims` parameter to Keycloak server during login.
* Values should correspond to the ACR levels defined in the ACR to Loa mapping for realm or client or to the numbers (levels) inside defined
* Keycloak authentication flow. See section 5.5.1 of OIDC 1.0 specification for the details.
*/
values: string[];
/**
* This parameter specifies if ACR claims is considered essential or not.
*/
essential: boolean;
}
export interface KeycloakInitOptions { export interface KeycloakInitOptions {
/** /**
* Adds a [cryptographic nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) * Adds a [cryptographic nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce)
@ -217,6 +230,11 @@ export interface KeycloakLoginOptions {
*/ */
loginHint?: string; loginHint?: string;
/**
* Sets the `acr` claim of the ID token sent inside the `claims` parameter. See section 5.5.1 of the OIDC 1.0 specification.
*/
acr?: Acr;
/** /**
* Used to tell Keycloak which IDP the user wants to authenticate with. * Used to tell Keycloak which IDP the user wants to authenticate with.
*/ */

View file

@ -378,6 +378,15 @@ function Keycloak (config) {
} }
} }
function buildClaimsParameter(requestedAcr){
var claims = {
id_token: {
acr: requestedAcr
}
}
return JSON.stringify(claims);
}
kc.createLoginUrl = function(options) { kc.createLoginUrl = function(options) {
var state = createUUID(); var state = createUUID();
var nonce = createUUID(); var nonce = createUUID();
@ -445,6 +454,11 @@ function Keycloak (config) {
url += '&ui_locales=' + encodeURIComponent(options.locale); url += '&ui_locales=' + encodeURIComponent(options.locale);
} }
if (options && options.acr) {
var claimsParameter = buildClaimsParameter(options.acr);
url += '&claims=' + encodeURIComponent(claimsParameter);
}
if (kc.pkceMethod) { if (kc.pkceMethod) {
var codeVerifier = generateCodeVerifier(96); var codeVerifier = generateCodeVerifier(96);
callbackState.pkceCodeVerifier = codeVerifier; callbackState.pkceCodeVerifier = codeVerifier;

View file

@ -1,8 +1,12 @@
package org.keycloak.testsuite.util.javascript; package org.keycloak.testsuite.util.javascript;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.keycloak.util.JsonSerialization;
/** /**
* @author mhajas * @author mhajas
*/ */
@ -100,7 +104,7 @@ public class JSObjectBuilder {
} }
private boolean skipQuotes(Object o) { private boolean skipQuotes(Object o) {
return (o instanceof Integer || o instanceof Boolean); return (o instanceof Integer || o instanceof Boolean || o instanceof JSObjectBuilder);
} }
public String build() { public String build() {
@ -111,11 +115,19 @@ public class JSObjectBuilder {
.append(option.getKey()) .append(option.getKey())
.append(" : "); .append(" : ");
if (!skipQuotes(option.getValue())) argument.append("\""); if (option.getValue().getClass().isArray()) {
try {
argument.append(JsonSerialization.writeValueAsString(option.getValue()));
} catch (IOException ioe) {
throw new IllegalArgumentException("Not possible to serialize value of the option " + option.getKey(), ioe);
}
} else {
if (!skipQuotes(option.getValue())) argument.append("\"");
argument.append(option.getValue()); argument.append(option.getValue());
if (!skipQuotes(option.getValue())) argument.append("\""); if (!skipQuotes(option.getValue())) argument.append("\"");
}
comma = ","; comma = ",";
} }
@ -124,5 +136,8 @@ public class JSObjectBuilder {
return argument.toString(); return argument.toString();
} }
@Override
public String toString() {
return build();
}
} }

View file

@ -9,8 +9,12 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.util.Retry; import org.keycloak.common.util.Retry;
import org.keycloak.common.util.UriUtils;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.ClaimsRepresentation;
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.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -33,11 +37,14 @@ import org.keycloak.testsuite.util.javascript.JSObjectBuilder;
import org.keycloak.testsuite.util.javascript.JavascriptStateValidator; import org.keycloak.testsuite.util.javascript.JavascriptStateValidator;
import org.keycloak.testsuite.util.javascript.JavascriptTestExecutor; import org.keycloak.testsuite.util.javascript.JavascriptTestExecutor;
import org.keycloak.testsuite.util.javascript.XMLHttpRequest; import org.keycloak.testsuite.util.javascript.XMLHttpRequest;
import org.keycloak.util.JsonSerialization;
import org.openqa.selenium.TimeoutException; import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import java.io.IOException;
import java.net.URL;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -498,6 +505,52 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
}); });
} }
/**
* Test for acr handling via {@code loginOptions}: <pre>{@code
* Keycloak keycloak = new Keycloak(); keycloak.login({.... acr: { values: ["foo", "bar"], essential: false}})
* }</pre>
*/
@Test
public void testAcrInLoginOptionsShouldBeConsideredByLoginUrl() {
// Test when no "acr" option given. Claims parameter won't be passed to Keycloak server
testExecutor.configure().init(defaultArguments());
JSObjectBuilder loginOptions = JSObjectBuilder.create();
testExecutor.login(loginOptions, (JavascriptStateValidator) (driver, output, events) -> {
try {
String queryString = new URL(driver.getCurrentUrl()).getQuery();
String claimsParam = UriUtils.decodeQueryString(queryString).getFirst(OIDCLoginProtocol.CLAIMS_PARAM);
Assert.assertNull(claimsParam);
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
});
// Test given "acr" option will be translated into the "claims" parameter passed to Keycloak server
jsDriver.navigate().to(testAppUrl);
testExecutor.configure().init(defaultArguments());
JSObjectBuilder acr1 = JSObjectBuilder.create()
.add("values", new String[] {"foo", "bar"})
.add("essential", false);
loginOptions = JSObjectBuilder.create().add("acr", acr1);
testExecutor.login(loginOptions, (JavascriptStateValidator) (driver, output, events) -> {
try {
String queryString = new URL(driver.getCurrentUrl()).getQuery();
String claimsParam = UriUtils.decodeQueryString(queryString).getFirst(OIDCLoginProtocol.CLAIMS_PARAM);
Assert.assertNotNull(claimsParam);
ClaimsRepresentation claimsRep = JsonSerialization.readValue(claimsParam, ClaimsRepresentation.class);
ClaimsRepresentation.ClaimValue<String> claimValue = claimsRep.getClaimValue(IDToken.ACR, ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
Assert.assertNames(claimValue.getValues(), "foo", "bar");
Assert.assertThat(claimValue.isEssential(), is(false));
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
});
}
@Test @Test
public void testUpdateToken() { public void testUpdateToken() {
XMLHttpRequest request = XMLHttpRequest.create() XMLHttpRequest request = XMLHttpRequest.create()