Skip to content
Open
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
Expand Up @@ -118,6 +118,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 @@ -200,6 +201,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ 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<@RedirectURI(message = "not a valid URL",
groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class,
OnClientCreation.class, OnClientUpdate.class}) String> postLogoutRedirectUris;

@Size(max = 2048,
groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class,
OnClientCreation.class, OnClientUpdate.class})
Expand Down Expand Up @@ -333,6 +340,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* 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.authn;

import java.io.IOException;
import java.text.ParseException;
import java.util.List;
import java.util.Optional;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mitre.jwt.assertion.impl.SelfAssertionValidator;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;

import it.infn.mw.iam.persistence.repository.client.IamClientRepository;

@Component
public class OidcLogoutSuccessHandler implements LogoutSuccessHandler {

private static final Logger LOG = LoggerFactory.getLogger(OidcLogoutSuccessHandler.class);

private IamClientRepository clientRepo;
private SelfAssertionValidator validator;

public OidcLogoutSuccessHandler(IamClientRepository clientRepo,
SelfAssertionValidator validator) {
this.clientRepo = clientRepo;
this.validator = validator;
}

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {

String idTokenHint = request.getParameter("id_token_hint");
String redirectUri = request.getParameter("post_logout_redirect_uri");
String state = request.getParameter("state");

String fallback = "/login?logout";

if (idTokenHint == null || redirectUri == null) {
LOG.debug("OIDC logout: missing id_token_hint or post_logout_redirect_uri");
response.sendRedirect(fallback);
return;
}

try {
JWT idToken = JWTParser.parse(idTokenHint);

if (!validator.isValid(idToken)) {
LOG.debug("OIDC logout: id_token_hint validation failed");
response.sendRedirect(fallback);
return;
}

JWTClaimsSet idTokenClaims = idToken.getJWTClaimsSet();
List<String> audience = idTokenClaims.getAudience();

Optional<ClientDetailsEntity> client = audience.stream()
.map(clientRepo::findByClientId)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();

if (client.isEmpty()) {
LOG.debug("OIDC logout: no matching client found in audiences {}", audience);
response.sendRedirect(fallback);
return;
}

if (!client.get().getPostLogoutRedirectUris().contains(redirectUri)) {
LOG.debug("OIDC logout redirect denied: invalid redirect URI {}", redirectUri);
response.sendRedirect(fallback);
return;
}

UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(redirectUri);

if (state != null) {
uriBuilder.queryParam("state", state);
}

response.sendRedirect(uriBuilder.toUriString());

} catch (ParseException e) {
LOG.debug("OIDC logout: invalid id_token_hint format", e);
response.sendRedirect(fallback);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import it.infn.mw.iam.authn.CheckMultiFactorIsEnabledSuccessHandler;
import it.infn.mw.iam.authn.ExternalAuthenticationHintService;
import it.infn.mw.iam.authn.HintAwareAuthenticationEntryPoint;
import it.infn.mw.iam.authn.OidcLogoutSuccessHandler;
import it.infn.mw.iam.authn.multi_factor_authentication.ExtendedAuthenticationFilter;
import it.infn.mw.iam.authn.multi_factor_authentication.ExtendedHttpServletRequestFilter;
import it.infn.mw.iam.authn.multi_factor_authentication.MultiFactorVerificationFilter;
Expand Down Expand Up @@ -144,6 +145,9 @@ public static class UserLoginConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IamProperties iamProperties;

@Autowired
private OidcLogoutSuccessHandler oidcLogoutSuccessHandler;

@Autowired
private IamTotpMfaProperties iamTotpMfaProperties;

Expand Down Expand Up @@ -178,7 +182,6 @@ protected AuthenticationEntryPoint entryPoint() {
return new HintAwareAuthenticationEntryPoint(delegate, hintService, aarcHintService);
}


@Override
protected void configure(final HttpSecurity http) throws Exception {

Expand Down Expand Up @@ -214,6 +217,7 @@ protected void configure(final HttpSecurity http) throws Exception {
.addFilterAfter(extendedHttpServletRequestFilter(), UsernamePasswordAuthenticationFilter.class)
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(oidcLogoutSuccessHandler)
.and().anonymous()
.and()
.csrf()
Expand All @@ -223,12 +227,12 @@ protected void configure(final HttpSecurity http) throws Exception {
}

@Bean
public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler() {
OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler() {
return new OAuth2WebSecurityExpressionHandler();
}

@Bean
public AuthenticationSuccessHandlerHelper authenticationSuccessHandlerHelper() {
AuthenticationSuccessHandlerHelper authenticationSuccessHandlerHelper() {
return new AuthenticationSuccessHandlerHelper(accountUtils, iamBaseUrl,
aupSignatureCheckService, accountRepo, iamTotpMfaService, iamTotpMfaProperties);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class IamWellKnownInfoProvider implements WellKnownInfoProvider {
public static final String TOKEN_ENDPOINT = "token";
public static final String ABOUT_ENDPOINT = "about";
public static final String SCIM_ENDPOINT = "scim";
public static final String LOGOUT_ENDPOINT = "logout";

private static final List<String> TOKEN_ENDPOINT_AUTH_METHODS = newArrayList(
"client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none");
Expand Down Expand Up @@ -106,6 +107,7 @@ public class IamWellKnownInfoProvider implements WellKnownInfoProvider {
private final String deviceAuthorizationEndpoint;
private final String aboutEndpoint;
private final String scimEndpoint;
private final String logoutEndpoint;
private Set<String> supportedScopes;


Expand Down Expand Up @@ -134,6 +136,7 @@ public IamWellKnownInfoProvider(IamProperties properties,
deviceAuthorizationEndpoint = buildEndpointUrl(DeviceEndpoint.URL);
aboutEndpoint = buildEndpointUrl(ABOUT_ENDPOINT);
scimEndpoint = buildEndpointUrl(SCIM_ENDPOINT);
logoutEndpoint = buildEndpointUrl(LOGOUT_ENDPOINT);
updateSupportedScopes();
}

Expand Down Expand Up @@ -221,6 +224,8 @@ public Map<String, Object> getWellKnownInfo() {
updateSupportedScopes();
result.put("scopes_supported", supportedScopes);

result.put("end_session_endpoint", logoutEndpoint);

return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
this client" validator="$ctrl.validRedirectURI()">
</inputlist>
<hr>
<inputlist id="postLogoutRedirectUri" model="$ctrl.client.post_logout_redirect_uris" placeholder="https://app.example.org"
label="Post Logout Redirect URIs" helptext="List of Post Logout Redirect URIs for this client">
</inputlist>
<hr>
<inputlist id="contacts" model="$ctrl.client.contacts" placeholder="administrator@example.org" label="Contacts"
helptext="List of email
address contacts for administrators of this
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* 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.test.logout;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import java.util.Base64;
import java.util.Optional;
import java.util.Set;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mitre.jwt.assertion.impl.SelfAssertionValidator;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import com.nimbusds.jwt.JWT;

import it.infn.mw.iam.authn.OidcLogoutSuccessHandler;
import it.infn.mw.iam.persistence.repository.client.IamClientRepository;

@ExtendWith(MockitoExtension.class)
class OidcLogoutSuccessHanlderTests {

@Mock
private IamClientRepository clientRepo;

@Mock
private SelfAssertionValidator validator;

private OidcLogoutSuccessHandler handler;

private MockHttpServletRequest request;
private MockHttpServletResponse response;

@BeforeEach
void setup() {
handler = new OidcLogoutSuccessHandler(clientRepo, validator);
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
}

private void mockClient(String clientId, Set<String> redirectUris) {
ClientDetailsEntity client = new ClientDetailsEntity();
client.setClientId(clientId);
client.setPostLogoutRedirectUris(redirectUris);

when(clientRepo.findByClientId(clientId)).thenReturn(Optional.of(client));
}

private String validJwtForClient(String clientId) {
return "eyJhbGciOiJub25lIn0." + Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(("{\"aud\":[\"" + clientId + "\"]}").getBytes()) + ".";
}

@Test
void missingParametersRedirectsToFallback() throws Exception {
handler.onLogoutSuccess(request, response, null);

assertEquals("/login?logout", response.getRedirectedUrl());
}

@Test
void invalidIdTokenRedirectsToFallback() throws Exception {
request.setParameter("id_token_hint", "not-a-jwt");
request.setParameter("post_logout_redirect_uri", "https://rp.example.org");

handler.onLogoutSuccess(request, response, null);

assertEquals("/login?logout", response.getRedirectedUrl());
}

@Test
void tokenValidationFailsRedirectsToFallback() throws Exception {
String jwt = "eyJhbGciOiJub25lIn0.eyJhdWQiOlsiY2xpZW50Il19.";

request.setParameter("id_token_hint", jwt);
request.setParameter("post_logout_redirect_uri", "https://rp.example.org");

when(validator.isValid(any(JWT.class))).thenReturn(false);

handler.onLogoutSuccess(request, response, null);

assertEquals("/login?logout", response.getRedirectedUrl());
}

@Test
void redirectUriNotRegisteredRedirectsToFallback() throws Exception {
String jwt = validJwtForClient("client");

request.setParameter("id_token_hint", jwt);
request.setParameter("post_logout_redirect_uri", "https://evil.example.org");

when(validator.isValid(any(JWT.class))).thenReturn(true);
mockClient("client", Set.of("https://rp.example.org/logout"));

handler.onLogoutSuccess(request, response, null);

assertEquals("/login?logout", response.getRedirectedUrl());
}

@Test
void validLogoutRedirectsToPostLogoutUri() throws Exception {
String jwt = validJwtForClient("client");

request.setParameter("id_token_hint", jwt);
request.setParameter("post_logout_redirect_uri", "https://rp.example.org/logout");
request.setParameter("state", "xyz");

when(validator.isValid(any(JWT.class))).thenReturn(true);
mockClient("client", Set.of("https://rp.example.org/logout"));

handler.onLogoutSuccess(request, response, null);

assertEquals("https://rp.example.org/logout?state=xyz", response.getRedirectedUrl());
}
}
Loading