Skip to content

Commit

Permalink
ACR support in the javascript adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
mposolda committed Feb 24, 2022
1 parent 6bce8b8 commit 52712d2
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 5 deletions.
18 changes: 18 additions & 0 deletions adapters/oidc/js/dist/keycloak.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
*/
Expand Down
14 changes: 14 additions & 0 deletions adapters/oidc/js/src/keycloak.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -100,7 +104,7 @@ private JSObjectBuilder pkceMethod(String method) {
}

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() {
Expand All @@ -111,11 +115,19 @@ public String build() {
.append(option.getKey())
.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 = ",";
}

Expand All @@ -124,5 +136,8 @@ public String build() {
return argument.toString();
}


@Override
public String toString() {
return build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
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;
Expand All @@ -33,11 +37,14 @@
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;

Expand Down Expand Up @@ -498,6 +505,52 @@ public void testScopeInLoginOptionsShouldBeConsideredByLoginUrl() {
});
}

/**
* 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()
Expand Down

0 comments on commit 52712d2

Please sign in to comment.