Skip to content

Commit a922e0c

Browse files
authored
Merge pull request #20088 from ayoho/20059-op-uriValidation
Issue 20059: Add validation for OAuth client backchannel logout URI
2 parents fe0dda0 + 85df7ae commit a922e0c

File tree

3 files changed

+224
-3
lines changed

3 files changed

+224
-3
lines changed

dev/com.ibm.ws.security.oauth/resources/com/ibm/ws/security/oauth20/internal/resources/OAuthMessages.nlsprops

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
###############################################################################
2-
# Copyright (c) 2012 IBM Corporation and others.
2+
# Copyright (c) 2012, 2022 IBM Corporation and others.
33
# All rights reserved. This program and the accompanying materials
44
# are made available under the terms of the Eclipse Public License v1.0
55
# which accompanies this distribution, and is available at
@@ -16,7 +16,7 @@
1616
#NLS_ENCODING=UNICODE
1717
# -------------------------------------------------------------------------------------------------
1818

19-
# Message prefix block: CWWKS1400 - CWWKS1499
19+
# Message prefix block: CWWKS1400 - CWWKS1499, CWWKS2300 - CWWKS2349
2020
OAUTH_PROVIDER_CONFIG_INVALID=CWWKS1400E: The OAuth provider {0} configuration is not valid.
2121
OAUTH_PROVIDER_CONFIG_INVALID.explanation=Cannot get specified oauth provider configuration.
2222
OAUTH_PROVIDER_CONFIG_INVALID.useraction=Specify a valid oauth provider configuration.
@@ -458,6 +458,20 @@ WRONG_TOKEN_GRANTTYPE=CWWKS1497E: The token with grant type [{0}] is not allowed
458458
WRONG_TOKEN_GRANTTYPE.explanation=The token is not valid because its grant type is not allowed.
459459
WRONG_TOKEN_GRANTTYPE.useraction=Use a token with a grant type that is allowed.
460460

461+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_CONTAINS_FRAGMENT=CWWKS1498E: The [{0}] URI for the {1} client registration metadata field is not valid because it contains a fragment.
462+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_CONTAINS_FRAGMENT.explanation=The request cannot be completed because the URI must not contain a fragment.
463+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_CONTAINS_FRAGMENT.useraction=Review the property value in the request and remove the URI fragment.
464+
465+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_INVALID_SCHEME=CWWKS1499E: The [{0}] URI for the {1} client registration metadata field is not valid because it does not use the HTTP or HTTPS scheme.
466+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_INVALID_SCHEME.explanation=The URI value must use the HTTP or HTTPS scheme.
467+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_INVALID_SCHEME.useraction=Review the property value in the request and update it to use the HTTP or HTTPS scheme.
468+
469+
# NOTE: Start of new message prefix CWWKS2300 - 2349
470+
471+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_HTTP_SCHEME_CLIENT_NOT_CONFIDENTIAL=CWWKS2300E: The [{0}] URI for the {1} client registration metadata field is not valid because it uses the HTTP scheme but the [{2}] OAuth client is not a confidential client.
472+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_HTTP_SCHEME_CLIENT_NOT_CONFIDENTIAL.explanation=The URI value for the metadata field that is specified in the message may use the HTTP scheme only if the OAuth client is a confidential client. Otherwise, the HTTPS scheme must be used.
473+
OAUTH_CLIENT_REGISTRATION_VALUE_URI_HTTP_SCHEME_CLIENT_NOT_CONFIDENTIAL.useraction=Update the URI to use the HTTPS scheme, or update the OAuth client to be a confidential client.
474+
461475
# html title of logout page
462476
LOGOUT_PAGE_TITLE=Logout
463477
# html body of logout page

dev/com.ibm.ws.security.oauth/src/com/ibm/ws/security/oauth20/plugins/OidcBaseClientValidator.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2014, 2021 IBM Corporation and others.
2+
* Copyright (c) 2014, 2022 IBM Corporation and others.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -104,6 +104,8 @@ public OidcBaseClient validateCreateUpdate() throws OidcServerException {
104104

105105
validateOutputParameters();
106106

107+
validateBackchannelLogoutUri();
108+
107109
return this.client;
108110
}
109111

@@ -656,4 +658,44 @@ private void validateUris(JsonArray uris, String clientMetadataField) throws Oid
656658
}
657659
}
658660
}
661+
662+
/**
663+
* Validates the backchannelLogoutUri/backchannel_logout_uri attribute of the client. Per Section 2.2 of
664+
* https://openid.net/specs/openid-connect-backchannel-1_0.html:
665+
* 1. The back-channel logout URI MUST be an absolute URI as defined by Section 4.3 of [RFC3986].
666+
* 2. The back-channel logout URI MAY include an application/x-www-form-urlencoded formatted query component, per Section 3.4 of [RFC3986], which MUST be retained when adding additional query parameters.
667+
* 3. The back-channel logout URI MUST NOT include a fragment component.
668+
* 4. This URL SHOULD use the https scheme and MAY contain port, path, and query parameter components; however, it MAY use the http scheme, provided that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0 [RFC6749], and provided the OP allows the use of http RP URIs.
669+
*/
670+
void validateBackchannelLogoutUri() throws OidcServerException {
671+
URI uri;
672+
String logoutUri = client.getBackchannelLogoutUri();
673+
if (logoutUri == null) {
674+
return;
675+
}
676+
try {
677+
uri = new URI(logoutUri);
678+
} catch (URISyntaxException e) {
679+
throw new OidcServerException(new BrowserAndServerLogMessage(tc, "OAUTH_CLIENT_REGISTRATION_VALUE_MALFORMED_URI", new Object[] { logoutUri, OidcBaseClient.SN_BACKCHANNEL_LOGOUT_URI }),
680+
OIDCConstants.ERROR_INVALID_CLIENT_METADATA, HttpServletResponse.SC_BAD_REQUEST, e);
681+
}
682+
if (!uri.isAbsolute()) {
683+
throw new OidcServerException(new BrowserAndServerLogMessage(tc, "OAUTH_CLIENT_REGISTRATION_VALUE_NOT_ABSOLUTE_URI", new Object[] { logoutUri, OidcBaseClient.SN_BACKCHANNEL_LOGOUT_URI }),
684+
OIDCConstants.ERROR_INVALID_CLIENT_METADATA, HttpServletResponse.SC_BAD_REQUEST);
685+
}
686+
if (uri.getFragment() != null) {
687+
throw new OidcServerException(new BrowserAndServerLogMessage(tc, "OAUTH_CLIENT_REGISTRATION_VALUE_URI_CONTAINS_FRAGMENT", new Object[] { logoutUri, OidcBaseClient.SN_BACKCHANNEL_LOGOUT_URI }),
688+
OIDCConstants.ERROR_INVALID_CLIENT_METADATA, HttpServletResponse.SC_BAD_REQUEST);
689+
}
690+
String scheme = uri.getScheme();
691+
if (scheme == null || (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
692+
throw new OidcServerException(new BrowserAndServerLogMessage(tc, "OAUTH_CLIENT_REGISTRATION_VALUE_URI_INVALID_SCHEME", new Object[] { logoutUri, OidcBaseClient.SN_BACKCHANNEL_LOGOUT_URI }),
693+
OIDCConstants.ERROR_INVALID_CLIENT_METADATA, HttpServletResponse.SC_BAD_REQUEST);
694+
}
695+
if (scheme.equalsIgnoreCase("http") && !client.isConfidential()) {
696+
throw new OidcServerException(new BrowserAndServerLogMessage(tc, "OAUTH_CLIENT_REGISTRATION_VALUE_URI_HTTP_SCHEME_CLIENT_NOT_CONFIDENTIAL", new Object[] { logoutUri, OidcBaseClient.SN_BACKCHANNEL_LOGOUT_URI, client.getClientId() }),
697+
OIDCConstants.ERROR_INVALID_CLIENT_METADATA, HttpServletResponse.SC_BAD_REQUEST);
698+
}
699+
}
700+
659701
}

dev/com.ibm.ws.security.oauth/test/com/ibm/ws/security/oauth20/plugins/OidcBaseClientValidatorTest.java

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package com.ibm.ws.security.oauth20.plugins;
1212

1313
import static org.junit.Assert.assertEquals;
14+
import static org.junit.Assert.assertTrue;
1415
import static org.junit.Assert.fail;
1516

1617
import javax.servlet.http.HttpServletResponse;
@@ -36,12 +37,14 @@ public class OidcBaseClientValidatorTest {
3637
private final String redirectUri2 = "https://localhost:8999/resource/redirect2";
3738
private final JsonArray redirectUris = new JsonArray();
3839
private OidcBaseClient client;
40+
private OidcBaseClient publicClient;
3941

4042
@Before
4143
public void setUp() {
4244
redirectUris.add(new JsonPrimitive(redirectUri1));
4345
redirectUris.add(new JsonPrimitive(redirectUri2));
4446
client = new OidcBaseClient(clientId, clientSecret, redirectUris, clientName, componentId, true);
47+
publicClient = new OidcBaseClient(clientId, null, redirectUris, clientName, componentId, true);
4548
}
4649

4750
@Test
@@ -320,4 +323,166 @@ public void testOutputParamtersPresent3() throws OidcServerException {
320323
assertEquals(expectedErrorMessage, e.getErrorDescription());
321324
}
322325
}
326+
327+
@Test
328+
public void testValidateBackchannelLogoutUri_nullUri() {
329+
client.setBackchannelLogoutUri(null);
330+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
331+
try {
332+
validator.validateBackchannelLogoutUri();
333+
} catch (OidcServerException e) {
334+
fail("Threw OidcServerException but didn't expect to: " + e.getErrorDescription());
335+
}
336+
}
337+
338+
@Test
339+
public void testValidateBackchannelLogoutUri_nonUri() {
340+
String uri = "This is not a valid URI";
341+
client.setBackchannelLogoutUri(uri);
342+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
343+
try {
344+
validator.validateBackchannelLogoutUri();
345+
fail("Did not throw OidcServerException as expected.");
346+
} catch (OidcServerException e) {
347+
assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getHttpStatus());
348+
assertEquals(OIDCConstants.ERROR_INVALID_CLIENT_METADATA, e.getErrorCode());
349+
String expectedRegex = "CWWKS1445E" + ".*" + uri + ".*";
350+
String errorDescription = e.getErrorDescription();
351+
assertTrue("Did not find error message \"" + expectedRegex + "\" in error description \"" + errorDescription + "\".", errorDescription.matches(expectedRegex));
352+
}
353+
}
354+
355+
@Test
356+
public void testValidateBackchannelLogoutUri_uriNotAbsolute() {
357+
String uri = "/some/path";
358+
client.setBackchannelLogoutUri(uri);
359+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
360+
try {
361+
validator.validateBackchannelLogoutUri();
362+
fail("Did not throw OidcServerException as expected.");
363+
} catch (OidcServerException e) {
364+
assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getHttpStatus());
365+
assertEquals(OIDCConstants.ERROR_INVALID_CLIENT_METADATA, e.getErrorCode());
366+
String expectedRegex = "CWWKS1446E" + ".*" + uri + ".*";
367+
String errorDescription = e.getErrorDescription();
368+
assertTrue("Did not find error message \"" + expectedRegex + "\" in error description \"" + errorDescription + "\".", errorDescription.matches(expectedRegex));
369+
}
370+
}
371+
372+
@Test
373+
public void testValidateBackchannelLogoutUri_uriContainsEmptyFragment() {
374+
String uri = "https://localhost/some/path#";
375+
client.setBackchannelLogoutUri(uri);
376+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
377+
try {
378+
validator.validateBackchannelLogoutUri();
379+
fail("Did not throw OidcServerException as expected.");
380+
} catch (OidcServerException e) {
381+
assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getHttpStatus());
382+
assertEquals(OIDCConstants.ERROR_INVALID_CLIENT_METADATA, e.getErrorCode());
383+
String expectedRegex = "CWWKS1498E" + ".*" + uri + ".*";
384+
String errorDescription = e.getErrorDescription();
385+
assertTrue("Did not find error message \"" + expectedRegex + "\" in error description \"" + errorDescription + "\".", errorDescription.matches(expectedRegex));
386+
}
387+
}
388+
389+
@Test
390+
public void testValidateBackchannelLogoutUri_uriContainsNonEmptyFragment() {
391+
String uri = "https://localhost/some/path#with-fragment";
392+
client.setBackchannelLogoutUri(uri);
393+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
394+
try {
395+
validator.validateBackchannelLogoutUri();
396+
fail("Did not throw OidcServerException as expected.");
397+
} catch (OidcServerException e) {
398+
assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getHttpStatus());
399+
assertEquals(OIDCConstants.ERROR_INVALID_CLIENT_METADATA, e.getErrorCode());
400+
String expectedRegex = "CWWKS1498E" + ".*" + uri + ".*";
401+
String errorDescription = e.getErrorDescription();
402+
assertTrue("Did not find error message \"" + expectedRegex + "\" in error description \"" + errorDescription + "\".", errorDescription.matches(expectedRegex));
403+
}
404+
}
405+
406+
@Test
407+
public void testValidateBackchannelLogoutUri_uriDoesNotUseValidScheme() {
408+
String uri = "file://localhost/some/path";
409+
client.setBackchannelLogoutUri(uri);
410+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
411+
try {
412+
validator.validateBackchannelLogoutUri();
413+
fail("Did not throw OidcServerException as expected.");
414+
} catch (OidcServerException e) {
415+
assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getHttpStatus());
416+
assertEquals(OIDCConstants.ERROR_INVALID_CLIENT_METADATA, e.getErrorCode());
417+
String expectedRegex = "CWWKS1499E" + ".*" + uri + ".*";
418+
String errorDescription = e.getErrorDescription();
419+
assertTrue("Did not find error message \"" + expectedRegex + "\" in error description \"" + errorDescription + "\".", errorDescription.matches(expectedRegex));
420+
}
421+
}
422+
423+
@Test
424+
public void testValidateBackchannelLogoutUri_httpUriClientNotConfidential() {
425+
String uri = "http://localhost/some/path";
426+
publicClient.setBackchannelLogoutUri(uri);
427+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(publicClient);
428+
try {
429+
validator.validateBackchannelLogoutUri();
430+
fail("Did not throw OidcServerException as expected.");
431+
} catch (OidcServerException e) {
432+
assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getHttpStatus());
433+
assertEquals(OIDCConstants.ERROR_INVALID_CLIENT_METADATA, e.getErrorCode());
434+
String expectedRegex = "CWWKS2300E" + ".*" + uri + ".*";
435+
String errorDescription = e.getErrorDescription();
436+
assertTrue("Did not find error message \"" + expectedRegex + "\" in error description \"" + errorDescription + "\".", errorDescription.matches(expectedRegex));
437+
}
438+
}
439+
440+
@Test
441+
public void testValidateBackchannelLogoutUri_simpleValidHttpUri() {
442+
String uri = "http://localhost/some/path";
443+
client.setBackchannelLogoutUri(uri);
444+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
445+
try {
446+
validator.validateBackchannelLogoutUri();
447+
} catch (OidcServerException e) {
448+
fail("Threw OidcServerException but didn't expect to: " + e.getErrorDescription());
449+
}
450+
}
451+
452+
@Test
453+
public void testValidateBackchannelLogoutUri_simpleValidHttpUri_uppercase() {
454+
String uri = "HTTP://LOCALHOST/SOME/PATH";
455+
client.setBackchannelLogoutUri(uri);
456+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
457+
try {
458+
validator.validateBackchannelLogoutUri();
459+
} catch (OidcServerException e) {
460+
fail("Threw OidcServerException but didn't expect to: " + e.getErrorDescription());
461+
}
462+
}
463+
464+
@Test
465+
public void testValidateBackchannelLogoutUri_simpleValidHttpsUri() {
466+
String uri = "https://localhost:9080/some/path";
467+
client.setBackchannelLogoutUri(uri);
468+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
469+
try {
470+
validator.validateBackchannelLogoutUri();
471+
} catch (OidcServerException e) {
472+
fail("Threw OidcServerException but didn't expect to: " + e.getErrorDescription());
473+
}
474+
}
475+
476+
@Test
477+
public void testValidateBackchannelLogoutUri_complexUri() {
478+
String uri = "https://localhost:9080/some/path?with=params&other=things";
479+
client.setBackchannelLogoutUri(uri);
480+
OidcBaseClientValidator validator = OidcBaseClientValidator.getInstance(client);
481+
try {
482+
validator.validateBackchannelLogoutUri();
483+
} catch (OidcServerException e) {
484+
fail("Threw OidcServerException but didn't expect to: " + e.getErrorDescription());
485+
}
486+
}
487+
323488
}

0 commit comments

Comments
 (0)