Skip to content

Commit 1121045

Browse files
strehleCopilot
andauthored
feature: add flag omitIdTokenHintOnLogout in OIDC config (#3711)
* feature: add flag omitIdTokenHintOnLogout in OIDC config Omit id_token_hint if omitIdTokenHintOnLogout: true Default is false and adds the id_token_hint during logout * Update server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthLogoutSuccessHandler.java ok, good to know Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * let copilot fix itself and generate tests. it is scary, but the inital code was generated by copilot, then review done and improved, but all from copilot * fix sonar --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 40de3f3 commit 1121045

File tree

6 files changed

+101
-1
lines changed

6 files changed

+101
-1
lines changed

model/src/main/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinition.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public class OIDCIdentityProviderDefinition extends AbstractExternalOAuthIdentit
3636
private boolean setForwardHeader;
3737
// Enable JWT Bearer Token Exchange Grant flow for this identity provider.
3838
private Boolean tokenExchangeEnabled;
39+
// Omit id_token_hint parameter in logout requests to this identity provider.
40+
private Boolean omitIdTokenHintOnLogout;
3941
@JsonInclude(JsonInclude.Include.NON_NULL)
4042
private List<Prompt> prompts;
4143
// Enables private_key_jwt towards identity provider.
@@ -104,6 +106,14 @@ public void setTokenExchangeEnabled(Boolean tokenExchangeEnabled) {
104106
this.tokenExchangeEnabled = tokenExchangeEnabled;
105107
}
106108

109+
public Boolean isOmitIdTokenHintOnLogout() {
110+
return omitIdTokenHintOnLogout;
111+
}
112+
113+
public void setOmitIdTokenHintOnLogout(Boolean omitIdTokenHintOnLogout) {
114+
this.omitIdTokenHintOnLogout = omitIdTokenHintOnLogout;
115+
}
116+
107117
@Override
108118
public Object clone() throws CloneNotSupportedException {
109119
return super.clone();
@@ -129,6 +139,9 @@ public boolean equals(Object o) {
129139
if (this.setForwardHeader != that.setForwardHeader) {
130140
return false;
131141
}
142+
if (!Objects.equals(this.omitIdTokenHintOnLogout, that.omitIdTokenHintOnLogout)) {
143+
return false;
144+
}
132145
if (!Objects.equals(this.jwtClientAuthentication, that.jwtClientAuthentication)) {
133146
return false;
134147
}
@@ -148,9 +161,25 @@ public int hashCode() {
148161
result = 31 * result + (discoveryUrl != null ? discoveryUrl.hashCode() : 0);
149162
result = 31 * result + (passwordGrantEnabled ? 1 : 0);
150163
result = 31 * result + (setForwardHeader ? 1 : 0);
164+
result = 31 * result + (omitIdTokenHintOnLogout != null ? omitIdTokenHintOnLogout.hashCode() : 0);
151165
result = 31 * result + (jwtClientAuthentication != null ? jwtClientAuthentication.hashCode() : 0);
152166
result = 31 * result + (additionalAuthzParameters != null ? additionalAuthzParameters.hashCode() : 0);
153167
result = 31 * result + (tokenExchangeEnabled != null ? tokenExchangeEnabled.hashCode() : 0);
154168
return result;
155169
}
170+
171+
@Override
172+
public String toString() {
173+
return "OIDCIdentityProviderDefinition{" +
174+
"discoveryUrl=" + discoveryUrl +
175+
", passwordGrantEnabled=" + passwordGrantEnabled +
176+
", setForwardHeader=" + setForwardHeader +
177+
", tokenExchangeEnabled=" + tokenExchangeEnabled +
178+
", omitIdTokenHintOnLogout=" + omitIdTokenHintOnLogout +
179+
", prompts=" + prompts +
180+
", jwtClientAuthentication=" + jwtClientAuthentication +
181+
", additionalAuthzParameters=" + additionalAuthzParameters +
182+
", parent=" + super.toString() +
183+
'}';
184+
}
156185
}

model/src/test/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinitionTests.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ void equalsTests() throws CloneNotSupportedException {
7373
OIDCIdentityProviderDefinition original = JsonUtils.readValue(defaultJson, OIDCIdentityProviderDefinition.class);
7474
OIDCIdentityProviderDefinition compare = (OIDCIdentityProviderDefinition) original.clone();
7575
compare.setTokenExchangeEnabled(false);
76+
compare.setOmitIdTokenHintOnLogout(false);
7677
assertThat(original).isNotEqualTo(compare);
7778
}
7879

@@ -88,4 +89,60 @@ void serialize_jwtClientAuthentication() {
8889
assertThat(def.getJwtClientAuthentication()).isEqualTo(settings);
8990
assertThat(def.getAuthMethod()).isNull();
9091
}
92+
93+
@Test
94+
void testToString() throws MalformedURLException {
95+
OIDCIdentityProviderDefinition def = new OIDCIdentityProviderDefinition();
96+
def.setDiscoveryUrl(URI.create(url).toURL());
97+
def.setPasswordGrantEnabled(true);
98+
def.setSetForwardHeader(true);
99+
def.setTokenExchangeEnabled(true);
100+
def.setOmitIdTokenHintOnLogout(true);
101+
102+
List<Prompt> prompts = Arrays.asList(
103+
new Prompt("username", "text", "Email"),
104+
new Prompt("password", "password", "Password")
105+
);
106+
def.setPrompts(prompts);
107+
108+
Map<String, String> jwtSettings = new HashMap<>();
109+
jwtSettings.put("iss", "issuer");
110+
def.setJwtClientAuthentication(jwtSettings);
111+
112+
Map<String, String> authzParams = new HashMap<>();
113+
authzParams.put("token_format", "jwt");
114+
def.setAdditionalAuthzParameters(authzParams);
115+
116+
String result = def.toString();
117+
118+
// Verify all attributes are present in toString output
119+
assertThat(result).contains("OIDCIdentityProviderDefinition{")
120+
.contains("discoveryUrl=" + def.getDiscoveryUrl())
121+
.contains("passwordGrantEnabled=true")
122+
.contains("setForwardHeader=true")
123+
.contains("tokenExchangeEnabled=true")
124+
.contains("omitIdTokenHintOnLogout=true")
125+
.contains("prompts=")
126+
.contains("jwtClientAuthentication=")
127+
.contains("additionalAuthzParameters=")
128+
.contains("parent=");
129+
}
130+
131+
@Test
132+
void testToStringWithNullValues() {
133+
OIDCIdentityProviderDefinition def = new OIDCIdentityProviderDefinition();
134+
135+
String result = def.toString();
136+
137+
// Verify toString works with null values
138+
assertThat(result).contains("OIDCIdentityProviderDefinition{")
139+
.contains("discoveryUrl=null")
140+
.contains("passwordGrantEnabled=false")
141+
.contains("setForwardHeader=false")
142+
.contains("tokenExchangeEnabled=null")
143+
.contains("omitIdTokenHintOnLogout=null")
144+
.contains("prompts=null")
145+
.contains("jwtClientAuthentication=null")
146+
.contains("additionalAuthzParameters=null");
147+
}
91148
}

server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthLogoutSuccessHandler.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ public String constructOAuthProviderLogoutUrl(final HttpServletRequest request,
7171
final String oauthLogoutUri = URLEncoder.encode(oauthLogoutUriBuilder.toString(), StandardCharsets.UTF_8);
7272

7373
String idTokenHint = "";
74-
if (authentication instanceof UaaAuthentication uaaAuthentication && uaaAuthentication.getIdpIdToken() != null) {
74+
boolean omitIdTokenHint = oauthConfig instanceof OIDCIdentityProviderDefinition oidcConfig
75+
&& Boolean.TRUE.equals(oidcConfig.isOmitIdTokenHintOnLogout());
76+
77+
if (!omitIdTokenHint && authentication instanceof UaaAuthentication uaaAuthentication && uaaAuthentication.getIdpIdToken() != null) {
7578
idTokenHint = "&id_token_hint=%s".formatted(uaaAuthentication.getIdpIdToken());
7679
}
7780

server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ private AbstractExternalOAuthIdentityProviderDefinition getExternalOIDCIdentityP
9696
idpDefinitionMap.get("passwordGrantEnabled") == null ? false : (boolean) idpDefinitionMap.get("passwordGrantEnabled"));
9797
oidcIdentityProviderDefinition.setSetForwardHeader(idpDefinitionMap.get("setForwardHeader") == null ? false : (boolean) idpDefinitionMap.get("setForwardHeader"));
9898
oidcIdentityProviderDefinition.setTokenExchangeEnabled(Optional.ofNullable(idpDefinitionMap.get("tokenExchangeEnabled")).filter(Boolean.class::isInstance).map(Boolean.class::cast).orElse(false));
99+
oidcIdentityProviderDefinition.setOmitIdTokenHintOnLogout(Optional.ofNullable(idpDefinitionMap.get("omitIdTokenHintOnLogout")).filter(Boolean.class::isInstance).map(Boolean.class::cast).orElse(null));
99100
oidcIdentityProviderDefinition.setPrompts((List<Prompt>) idpDefinitionMap.get("prompts"));
100101
setJwtClientAuthentication(idpDefinitionMap, oidcIdentityProviderDefinition);
101102
oauthIdpDefinitions.put(alias, oidcIdentityProviderDefinition);

server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthLogoutSuccessHandlerTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ void determineTargetUrlWithIdTokenHint() {
110110
.isEqualTo("http://localhost:8080/uaa/logout.do?post_logout_redirect_uri=http%3A%2F%2Flocalhost%3Fparameter%3Dvalue&client_id=id&id_token_hint=token");
111111
}
112112

113+
@Test
114+
void determineTargetUrlWithOmitIdTokenHint() {
115+
request.setQueryString("parameter=value");
116+
when(uaaAuthentication.getIdpIdToken()).thenReturn("token");
117+
oAuthIdentityProviderDefinition.setOmitIdTokenHintOnLogout(true);
118+
assertThat(oAuthLogoutHandler.determineTargetUrl(request, response, uaaAuthentication))
119+
.isEqualTo("http://localhost:8080/uaa/logout.do?post_logout_redirect_uri=http%3A%2F%2Flocalhost%3Fparameter%3Dvalue&client_id=id");
120+
}
121+
113122
@Test
114123
void determineDefaultTargetUrl() {
115124
oAuthIdentityProviderDefinition.setLogoutUrl(null);

uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,7 @@ void createOidcIdentityProvider() throws Exception {
704704
fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Only effective if relyingPartySecret is defined. Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header. It is recommended to set `jwtClientAuthentication:true` instead."),
705705
fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."),
706706
fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"),
707+
fieldWithPath("config.omitIdTokenHintOnLogout").optional(false).type(BOOLEAN).description("Omit the `id_token_hint` parameter when performing RP-Initiated Logout at the OIDC provider."),
707708
fieldWithPath("config.userInfoUrl").optional(null).type(OBJECT).description("Reserved for future OIDC use. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."),
708709
fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OIDC logout endpoint. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."),
709710
fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, defaults to `code`, but can be `code id_token` if the OIDC server can return an id_token as a query parameter in the redirect."),

0 commit comments

Comments
 (0)