Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a026f98
Add a custom oidc logout success handler
rmiccoli Feb 24, 2026
177d805
Add tests
rmiccoli Feb 24, 2026
8c1770b
Remove public modifier from test class
rmiccoli Feb 24, 2026
2934fc1
Consider multiple audience
rmiccoli Feb 24, 2026
72c9215
Merge remote-tracking branch 'origin/develop' into issue-1179
rmiccoli Mar 3, 2026
0f0b53e
Merge remote-tracking branch 'origin/develop' into issue-1179
rmiccoli Mar 27, 2026
e55b617
Merge remote-tracking branch 'origin/develop' into issue-1179
rmiccoli Jun 18, 2026
da55d38
Remove old redirect uri validator
rmiccoli Jun 19, 2026
1ecb9c8
Add PostLogoutRedirectUris validation and tests
enricovianello Jun 26, 2026
9b6a61c
Minor changes to the UI
enricovianello Jun 26, 2026
96cbd25
Fix Sonar issues
enricovianello Jun 26, 2026
7776ae4
Merge remote-tracking branch 'origin/develop' into issue-1179
rmiccoli Jul 1, 2026
4cf96fe
Fix sonar issues
rmiccoli Jul 1, 2026
1c61b53
Replace 3 tests with a single Parameterized one
rmiccoli Jul 1, 2026
13ab36c
Merge remote-tracking branch 'origin/develop' into issue-1179
rmiccoli Oct 2, 2026
608a8bb
Replace MITRE validator
rmiccoli Oct 2, 2026
5479c8c
Add tests for OidcIdTokenHintValidator
rmiccoli Oct 2, 2026
cc2774c
Fix sonar issues and add logs
rmiccoli Oct 3, 2026
fe4607a
Fix Sonar NPE warning
rmiccoli Oct 5, 2026
552c4bf
Create ValidPostLogoutRedirectURIsValidator
rmiccoli Oct 5, 2026
61b6f1f
Fix and add tests
rmiccoli Oct 5, 2026
eb795e4
Fix sonar issue
rmiccoli Oct 5, 2026
b333a40
Merge remote-tracking branch 'origin/develop' into issue-1179
rmiccoli Oct 9, 2026
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,36 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.infn.mw.iam.api.client.registration.validation;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Retention(RUNTIME)
@Target(TYPE)
@Constraint(validatedBy = ValidPostLogoutRedirectURIsValidator.class)
public @interface ValidPostLogoutRedirectURIs {
String message() default "Invalid post logout redirect URIs configuration";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.infn.mw.iam.api.client.registration.validation;

import static java.lang.String.format;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import java.util.Set;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import it.infn.mw.iam.api.common.client.RegisteredClientDTO;
import it.infn.mw.iam.api.common.client.TokenEndpointAuthenticationMethod;
import it.infn.mw.iam.core.oauth.consent.BlockedUriService;

@Component
@Scope("prototype")
public class ValidPostLogoutRedirectURIsValidator
implements ConstraintValidator<ValidPostLogoutRedirectURIs, RegisteredClientDTO> {

private static final Set<String> ALLOWED_SCHEMES = Set.of("https", "http");
private static final Set<String> ALLOWED_HTTP_HOSTS =
Set.of("localhost", "127.0.0.1", "[::1]", "[0:0:0:0:0:0:0:1]");
private final BlockedUriService denyListService;

public ValidPostLogoutRedirectURIsValidator(BlockedUriService denyListService) {
this.denyListService = denyListService;
}

private boolean invalid(ConstraintValidatorContext context, String message) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
return false;
}

@Override
public boolean isValid(RegisteredClientDTO value, ConstraintValidatorContext context) {

if (!Objects.isNull(value.getPostLogoutRedirectUris())) {
for (String uri : value.getPostLogoutRedirectUris()) {
if (!isValid(uri, value, context)) {
return false;
}
}
}
return true;
}

private boolean isValid(String uri, RegisteredClientDTO value,
ConstraintValidatorContext context) {

URI parsedUri;

try {
parsedUri = new URI(uri);
} catch (URISyntaxException e) {
return invalid(context, "Invalid post logout redirect URI");
}

String scheme = parsedUri.getScheme();
if (scheme == null || !ALLOWED_SCHEMES.contains(scheme.toLowerCase())) {
return invalid(context, format("Invalid post logout redirect URI scheme: %s", scheme));
}

boolean isHttp = "http".equalsIgnoreCase(scheme);
boolean isLoopback = ALLOWED_HTTP_HOSTS.contains(parsedUri.getHost());
boolean isPublicClient =
TokenEndpointAuthenticationMethod.none.equals(value.getTokenEndpointAuthMethod());

if (isHttp && isPublicClient) {
return invalid(context,
"Plain http post logout redirect URIs are only allowed for confidential clients");
}

if (isHttp && !isLoopback) {
return invalid(context,
"Plain http post logout redirect URIs are only allowed for loopback hosts");
}

if (parsedUri.getFragment() != null) {
return invalid(context, "Invalid post logout redirect URI: contains a fragment");
}

if (denyListService.isBlockedUri(uri)) {
return invalid(context, format("Invalid post logout redirect URI: %s is not allowed", uri));
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,39 +55,46 @@ private boolean invalid(ConstraintValidatorContext context, String message) {

@Override
public boolean isValid(RegisteredClientDTO value, ConstraintValidatorContext context) {
if (Objects.isNull(value.getRedirectUris())) {
return true;
}

for (String uri : value.getRedirectUris()) {
URI parsedUri;
try {
parsedUri = new URI(uri);
} catch (URISyntaxException e) {
return invalid(context, "Invalid redirect URI");
if (!Objects.isNull(value.getRedirectUris())) {
for (String uri : value.getRedirectUris()) {
if (!isValid(uri, context)) {
return false;
}
}
}
return true;
}

if (ALLOWED_REDIRECT_URIS.contains(uri)) {
continue;
}
private boolean isValid(String uri, ConstraintValidatorContext context) {

String scheme = parsedUri.getScheme();
if (scheme == null || !ALLOWED_SCHEMES.contains(scheme.toLowerCase())) {
return invalid(context, format("Invalid redirect URI scheme: %s", scheme));
}
URI parsedUri;

if ("http".equalsIgnoreCase(scheme)
&& !ALLOWED_HOSTS_FOR_HTTP.contains(parsedUri.getHost())) {
return invalid(context, "Plain http redirect URIs are only allowed for loopback");
}
try {
parsedUri = new URI(uri);
} catch (URISyntaxException e) {
return invalid(context, "Invalid redirect URI");
}

if (parsedUri.getFragment() != null) {
return invalid(context, "Invalid redirect URI: contains a fragment");
}
if (ALLOWED_REDIRECT_URIS.contains(uri)) {
return true;
}

if (denyListService.isBlockedUri(uri)) {
return invalid(context, format("Invalid redirect URI: %s is not allowed", uri));
}
String scheme = parsedUri.getScheme();
if (scheme == null || !ALLOWED_SCHEMES.contains(scheme.toLowerCase())) {
return invalid(context, format("Invalid redirect URI scheme: %s", scheme));
}

if ("http".equalsIgnoreCase(scheme) && !ALLOWED_HOSTS_FOR_HTTP.contains(parsedUri.getHost())) {
return invalid(context, "Plain http redirect URIs are only allowed for loopback");
}

if (parsedUri.getFragment() != null) {
return invalid(context, "Invalid redirect URI: contains a fragment");
}

if (denyListService.isBlockedUri(uri)) {
return invalid(context, format("Invalid redirect URI: %s is not allowed", uri));
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
import it.infn.mw.iam.api.common.client.TokenEndpointAuthenticationMethod;
import it.infn.mw.iam.config.IamProperties;
import it.infn.mw.iam.config.client_registration.ClientRegistrationProperties;
import it.infn.mw.iam.persistence.model.IamFederatedClientEntity;
import it.infn.mw.iam.persistence.model.ClientAuthMethod;
import it.infn.mw.iam.persistence.model.ClientDetailsEntity;
import it.infn.mw.iam.persistence.model.IamFederatedClientEntity;

@Component
public class ClientConverter {
Expand All @@ -63,7 +63,6 @@ private <T> Set<T> cloneSet(Set<T> stringSet) {
return result;
}


public ClientDetailsEntity entityFromClientManagementRequest(RegisteredClientDTO dto)
throws ParseException {
ClientDetailsEntity client = entityFromRegistrationRequest(dto);
Expand All @@ -90,8 +89,8 @@ public ClientDetailsEntity entityFromClientManagementRequest(RegisteredClientDTO
}

if (dto.getTokenEndpointAuthMethod() != null) {
client
.setTokenEndpointAuthMethod(ClientAuthMethod.getByValue(dto.getTokenEndpointAuthMethod().name()));
client.setTokenEndpointAuthMethod(
ClientAuthMethod.getByValue(dto.getTokenEndpointAuthMethod().name()));
}

client.setRequireAuthTime(Boolean.valueOf(dto.isRequireAuthTime()));
Expand All @@ -100,8 +99,6 @@ public ClientDetailsEntity entityFromClientManagementRequest(RegisteredClientDTO
return client;
}



public RegisteredClientDTO registeredClientDtoFromEntity(ClientDetailsEntity entity) {
RegisteredClientDTO clientDTO = new RegisteredClientDTO();

Expand All @@ -117,6 +114,7 @@ public RegisteredClientDTO registeredClientDtoFromEntity(ClientDetailsEntity ent

clientDTO.setJwksUri(entity.getJwksUri());
clientDTO.setRedirectUris(cloneSet(entity.getRedirectUris()));
clientDTO.setPostLogoutRedirectUris(entity.getPostLogoutRedirectUris());

clientDTO.setTokenEndpointAuthMethod(TokenEndpointAuthenticationMethod
.valueOf(Optional.ofNullable(entity.getTokenEndpointAuthMethod())
Expand Down Expand Up @@ -198,6 +196,8 @@ public ClientDetailsEntity entityFromRegistrationRequest(RegisteredClientDTO dto

client.setRedirectUris(cloneSet(dto.getRedirectUris()));

client.setPostLogoutRedirectUris(dto.getPostLogoutRedirectUris());

client.setScope(cloneSet(dto.getScope()));

client.setGrantTypes(new HashSet<>());
Expand All @@ -209,7 +209,7 @@ public ClientDetailsEntity entityFromRegistrationRequest(RegisteredClientDTO dto
dto.getGrantTypes().stream().map(AuthorizationGrantType::getGrantType).collect(toSet()));
}

if (dto.getScope().contains("offline_access")) {
if (client.getScope().contains("offline_access")) {
client.getGrantTypes().add(AuthorizationGrantType.REFRESH_TOKEN.getGrantType());
}

Expand All @@ -221,8 +221,8 @@ public ClientDetailsEntity entityFromRegistrationRequest(RegisteredClientDTO dto
client.setContacts(cloneSet(dto.getContacts()));

if (!isNull(dto.getTokenEndpointAuthMethod())) {
client
.setTokenEndpointAuthMethod(ClientAuthMethod.getByValue(dto.getTokenEndpointAuthMethod().name()));
client.setTokenEndpointAuthMethod(
ClientAuthMethod.getByValue(dto.getTokenEndpointAuthMethod().name()));
}

if (dto.getCodeChallengeMethod() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import it.infn.mw.iam.api.client.registration.validation.OnDynamicClientRegistration;
import it.infn.mw.iam.api.client.registration.validation.OnDynamicClientUpdate;
import it.infn.mw.iam.api.client.registration.validation.ValidGrantType;
import it.infn.mw.iam.api.client.registration.validation.ValidPostLogoutRedirectURIs;
import it.infn.mw.iam.api.client.registration.validation.ValidRedirectURIs;
import it.infn.mw.iam.api.client.registration.validation.ValidTokenEndpointAuthMethod;
import it.infn.mw.iam.api.common.ClientViews;
Expand All @@ -56,6 +57,8 @@
OnDynamicClientRegistration.class, OnDynamicClientUpdate.class})
@ValidRedirectURIs(groups = {OnClientCreation.class, OnClientUpdate.class,
OnDynamicClientRegistration.class, OnDynamicClientUpdate.class})
@ValidPostLogoutRedirectURIs(groups = {OnClientCreation.class, OnClientUpdate.class,
OnDynamicClientRegistration.class, OnDynamicClientUpdate.class})
@ValidTokenEndpointAuthMethod(groups = {OnClientCreation.class, OnClientUpdate.class,
OnDynamicClientRegistration.class, OnDynamicClientUpdate.class})
/**
Expand Down Expand Up @@ -115,6 +118,11 @@ public class RegisteredClientDTO {
ClientViews.NoSecretDynamicRegistration.class, ClientViews.DynamicRegistration.class})
private String clientUri;

@Valid
@JsonView({ClientViews.Limited.class, ClientViews.ClientManagement.class,
ClientViews.NoSecretDynamicRegistration.class, ClientViews.DynamicRegistration.class})
private Set<String> postLogoutRedirectUris;

@Size(max = 2048,
groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class,
OnClientCreation.class, OnClientUpdate.class})
Expand Down Expand Up @@ -324,6 +332,14 @@ public void setClientUri(String clientUri) {
this.clientUri = clientUri;
}

public Set<String> getPostLogoutRedirectUris() {
return postLogoutRedirectUris;
}

public void setPostLogoutRedirectUris(Set<String> postLogoutRedirectUris) {
this.postLogoutRedirectUris = postLogoutRedirectUris;
}

public String getTosUri() {
return tosUri;
}
Expand Down
Loading
Loading