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;
}
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 {
/**
* Adds a [cryptographic nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce)
@ -217,6 +230,11 @@ export interface KeycloakLoginOptions {
*/
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.
*/

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) {
var state = createUUID();
var nonce = createUUID();
@ -445,6 +454,11 @@ function Keycloak (config) {
url += '&ui_locales=' + encodeURIComponent(options.locale);
}
if (options && options.acr) {
var claimsParameter = buildClaimsParameter(options.acr);
url += '&claims=' + encodeURIComponent(claimsParameter);
}
if (kc.pkceMethod) {
var codeVerifier = generateCodeVerifier(96);
callbackState.pkceCodeVerifier = codeVerifier;

View file

@ -1,8 +1,12 @@
package org.keycloak.testsuite.util.javascript;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.util.JsonSerialization;
/**
* @author mhajas
*/
@ -100,7 +104,7 @@ public class JSObjectBuilder {
}
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() {
@ -111,11 +115,19 @@ public class JSObjectBuilder {
.append(option.getKey())
.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());
if (!skipQuotes(option.getValue())) argument.append("\"");
}
comma = ",";
}
@ -124,5 +136,8 @@ public class JSObjectBuilder {
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.common.Profile;
import org.keycloak.common.util.Retry;
import org.keycloak.common.util.UriUtils;
import org.keycloak.events.Details;
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.EventRepresentation;
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.JavascriptTestExecutor;
import org.keycloak.testsuite.util.javascript.XMLHttpRequest;
import org.keycloak.util.JsonSerialization;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import java.io.IOException;
import java.net.URL;
import java.util.List;
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
public void testUpdateToken() {
XMLHttpRequest request = XMLHttpRequest.create()