Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const assert = require("assert");
const cas = require("../../cas.js");

async function assertPageUrlDoesNotContain(page, url) {
const result = await page.url();
assert(!result.includes(url));
}

async function verifyLogoutWithIdTokenHint(clientId, redirectUrl, page) {
let params = "grant_type=client_credentials&";
params += `scope=${encodeURIComponent("openid")}`;
const tokenUrl = `https://localhost:8443/cas/oidc/token?${params}`;
await cas.log(`Calling ${tokenUrl} for client id ${clientId} to obtain an ID token...`);

const idToken = await cas.doPost(tokenUrl, "", {
"Content-Type": "application/json",
"Authorization": `Basic ${btoa(`${clientId}:secret`)}`
}, async (res) => {
await cas.log(res.data.id_token);
return res.data.id_token;
}, (error) => {
throw `Operation failed: ${error}`;
});

let logoutUrl = "https://localhost:8443/cas/oidc/oidcLogout";
logoutUrl += `?post_logout_redirect_uri=${redirectUrl}`;
logoutUrl += "&state=1234567890";
logoutUrl += `&client_id=${clientId}`;
logoutUrl += `&id_token_hint=${idToken}`;

// XXX Unable to handle custom URL schemes. This call causes a
// net::ERR_ABORTED error, which results in response === null.
const response = await cas.goto(page, logoutUrl, 1);
await cas.sleep(1000);
await cas.logPage(page);
if (response !== null) {
await cas.log(`${response.status()} ${response.statusText()}`);
}

// We can't assert anything positive about the resulting URL, but we can at
// least assert that the redirect URL is NOT treated as a relative path.
await assertPageUrlDoesNotContain(page, `/${redirectUrl}`);

await cas.gotoLogout(page, "https://localhost:9859/anything/oidc&client_id=whatever");
await cas.sleep(1000);
await cas.assertPageUrlStartsWith(page, "https://localhost:8443/cas/logout");
}

(async () => {
const casService = "https://localhost:9859/anything/cas";
const browser = await cas.newBrowser(cas.browserOptions());
const page = await cas.newPage(browser);
let response = await cas.gotoLogout(page, casService);
await cas.sleep(1000);
await cas.logPage(page);
await cas.log(`${response.status()} ${response.statusText()}`);
assert(response.ok());
await cas.assertPageUrl(page, casService);

response = await cas.goto(page, await page.url());
await cas.sleep(1000);
await cas.logPage(page);
await cas.log(`${response.status()} ${response.statusText()}`);
assert(response.status() === 200);
await cas.assertPageUrl(page, casService);

const customSchemeRedirect = "custom://localhost:9859/anything/oidc";
await verifyLogoutWithIdTokenHint("client", customSchemeRedirect, page);

const customIssuerService = "custom://localhost:9859/anything/customissuer";
await verifyLogoutWithIdTokenHint("customclient", customIssuerService, page);
await cas.closeBrowser(browser);
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"dependencies": "oidc",
"conditions": {
"docker": "true"
},
"properties": [
"--cas.server.name=https://localhost:8443",
"--cas.server.prefix=${cas.server.name}/cas",

"--cas.logout.follow-service-redirects=true",

"--cas.authn.oidc.core.issuer=https://localhost:8443/cas/oidc",
"--cas.authn.oidc.jwks.file-system.jwks-file=file:${#systemProperties['java.io.tmpdir']}/keystore.jwks",

"--cas.authn.oidc.discovery.claims=sub,name",
"--cas.authn.oidc.discovery.scopes=openid,profile,email",

"--cas.service-registry.core.init-from-json=true",
"--cas.service-registry.json.location=file:${PWD}/ci/tests/puppeteer/scenarios/${SCENARIO}/services"
],
"initScript": "${PWD}/ci/tests/httpbin/run-httpbin-server.sh"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"@class" : "org.apereo.cas.services.CasRegisteredService",
"serviceId" : "^https://localhost:9859/anything/cas.*",
"name" : "CAS",
"id" : 2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"@class" : "org.apereo.cas.services.OidcRegisteredService",
"clientId": "client",
"clientSecret": "secret",
"serviceId" : "^(https|custom)://localhost:9859/anything/oidc.*",
"name": "OIDC",
"id": 1,
"jwtAccessToken": "true",
"scopes" : [ "java.util.HashSet", ["openid"]]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"@class" : "org.apereo.cas.services.OidcRegisteredService",
"clientId": "customclient",
"clientSecret": "secret",
"idTokenIssuer": "https://localhost:8443",
"serviceId" : "^(https|custom)://localhost:9859/anything/customissuer.*",
"name": "OIDC",
"id": 3,
"jwtAccessToken": "true",
"scopes" : [ "java.util.HashSet", ["openid"]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.apereo.cas.CasProtocolConstants;
import org.apereo.cas.audit.AuditableContext;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.logout.slo.SingleLogoutUrl;
import org.apereo.cas.oidc.OidcConfigurationContext;
import org.apereo.cas.oidc.OidcConstants;
Expand Down Expand Up @@ -45,14 +46,18 @@
@Tag(name = "OpenID Connect")
public class OidcLogoutEndpointController extends BaseOidcController {

private final CasConfigurationProperties casProperties;

private final UrlValidator urlValidator;

private final OidcPostLogoutRedirectUrlMatcher postLogoutRedirectUrlMatcher;

public OidcLogoutEndpointController(final OidcConfigurationContext context,
public OidcLogoutEndpointController(final CasConfigurationProperties casProperties,
final OidcConfigurationContext context,
final OidcPostLogoutRedirectUrlMatcher postLogoutRedirectUrlMatcher,
final UrlValidator urlValidator) {
super(context);
this.casProperties = casProperties;
this.urlValidator = urlValidator;
this.postLogoutRedirectUrlMatcher = postLogoutRedirectUrlMatcher;
}
Expand Down Expand Up @@ -186,8 +191,18 @@ protected ResponseEntity executeLogoutRedirect(final Optional<String> state,
state.ifPresent(st -> builder.queryParam(OAuth20Constants.STATE, st));
clientId.ifPresent(id -> builder.queryParam(OAuth20Constants.CLIENT_ID, id));
val logoutUrl = builder.build().toUriString();
LOGGER.debug("Final logout redirect URL is [{}]", logoutUrl);
WebUtils.putLogoutRedirectUrl(request, logoutUrl);

// Redirect through the callbackAuthorize endpoint, to prevent custom URL schemes from being interpreted as
// relative paths. Otherwise, redirects intended for, e.g., custom://post_logout_redirect_uri would
// instead be redirected to https://localhost:8443/cas/custom://post_logout_redirect_uri.
val finalBuilder = UriComponentsBuilder.fromUriString(
OAuth20Utils.casOAuthCallbackUrl(casProperties.getServer().getPrefix()));
finalBuilder.queryParam("redirect_uri", logoutUrl);
clientId.ifPresent(id -> finalBuilder.queryParam(OAuth20Constants.CLIENT_ID, id));
val finalUrl = finalBuilder.encode().build().toUriString();

LOGGER.error("Final logout redirect URL is [{}]", finalUrl);
WebUtils.putLogoutRedirectUrl(request, finalUrl);
});
request.setAttribute("status", HttpStatus.PERMANENT_REDIRECT);
request.getServletContext()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,13 +352,14 @@ public OidcPushedAuthorizeEndpointController oidcPushedAuthorizeController(
@Bean
@ConditionalOnMissingBean(name = "oidcLogoutEndpointController")
public OidcLogoutEndpointController oidcLogoutEndpointController(
final CasConfigurationProperties casProperties,
@Qualifier(OidcPostLogoutRedirectUrlMatcher.BEAN_NAME_POST_LOGOUT_REDIRECT_URL_MATCHER)
final OidcPostLogoutRedirectUrlMatcher postLogoutRedirectUrlMatcher,
@Qualifier(UrlValidator.BEAN_NAME)
final UrlValidator urlValidator,
@Qualifier(OidcConfigurationContext.BEAN_NAME)
final OidcConfigurationContext oidcConfigurationContext) {
return new OidcLogoutEndpointController(oidcConfigurationContext,
return new OidcLogoutEndpointController(casProperties, oidcConfigurationContext,
postLogoutRedirectUrlMatcher, urlValidator);
}

Expand Down