Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
a9bed11
Implement automatic client registration
rmiccoli Oct 14, 2025
2f85ebe
Add verification of the request object signature
rmiccoli Oct 23, 2025
b3bbbac
Fix after rebase from develop
rmiccoli Oct 24, 2025
8bba0ea
Add missing license
rmiccoli Oct 24, 2025
d3ca114
Fix some code smells
rmiccoli Oct 24, 2025
a6e922e
Fix other code smells
rmiccoli Oct 24, 2025
290b240
Validate URL before constructing it
rmiccoli Oct 27, 2025
71ee991
Refactor methods to reduce complexity
rmiccoli Oct 28, 2025
1b57615
Combine common logic into an abstract class
rmiccoli Oct 31, 2025
7db6416
Fix code smell and move handler
rmiccoli Oct 31, 2025
921d25a
Add info in OP Entity Configuration
rmiccoli Oct 31, 2025
f028231
Add test
rmiccoli Oct 31, 2025
c1f4cb3
Add other tests
rmiccoli Nov 3, 2025
7e783a0
Refactor error responses
rmiccoli Nov 10, 2025
d4d1306
Replace mitreID client service
rmiccoli Nov 10, 2025
0b5a516
Fix client initialization
rmiccoli Nov 10, 2025
31eced6
Simplify fetchEntityConfiguration method
rmiccoli Nov 10, 2025
ac2155a
Delete the extra try
rmiccoli Nov 10, 2025
a48c2ce
Refactor code
rmiccoli Nov 11, 2025
a217194
Simplify doFilter "brain" method
rmiccoli Nov 11, 2025
1d5c272
Add tests
rmiccoli Nov 11, 2025
e096dc8
Refactor code and add tests
rmiccoli Nov 12, 2025
f9b590b
Increase coverage
rmiccoli Nov 12, 2025
8309548
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Nov 12, 2025
3b035af
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Nov 19, 2025
907f414
Run expired client task when oidfed profile is active
rmiccoli Nov 19, 2025
d3c6d1d
Fix sonar issues
rmiccoli Nov 19, 2025
eee86b6
Increase jwk size
rmiccoli Nov 24, 2025
53b0b72
Add a log during token request
rmiccoli Nov 24, 2025
c77e92d
Merge tag 'v1.13.1' into issue-1059
rmiccoli Nov 24, 2025
5c7c3ee
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Nov 24, 2025
d7ad42c
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Nov 24, 2025
13aac71
Avoid string concatenation
rmiccoli Nov 25, 2025
075042c
Increase jwk size
rmiccoli Nov 25, 2025
72fad1d
Add request object signing alg in clientDTO
rmiccoli Nov 25, 2025
81438ae
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Nov 26, 2025
c86fb05
Add filter to log basic auth header
rmiccoli Nov 27, 2025
96cd87c
Move the doFilter outside the debug block
rmiccoli Nov 27, 2025
9ddf01a
Extend test to check authz code flow works
rmiccoli Nov 28, 2025
0831138
Dump all headers
rmiccoli Nov 28, 2025
3b3388b
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Dec 18, 2025
7030d4e
Fix code smell
rmiccoli Dec 18, 2025
846db27
Fix request object signing algorithm
rmiccoli Jan 8, 2026
2be156e
Replace string with boolean
rmiccoli Jan 15, 2026
39b6a3e
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Jan 16, 2026
49515aa
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Jan 16, 2026
0e5cb5c
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Jan 23, 2026
60d764d
Update client when expired
rmiccoli Jan 23, 2026
7b237db
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Jan 23, 2026
059d41e
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Jan 29, 2026
503e080
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Feb 13, 2026
eeb5b98
Enable basic auth filter only for oidfed
rmiccoli Feb 13, 2026
3b2da29
Disable Docker BuildKit
rmiccoli Feb 13, 2026
2e59c7d
Improve workflow
rmiccoli Feb 13, 2026
2698f27
Revert "Enable basic auth filter only for oidfed"
rmiccoli Feb 13, 2026
d8b3bde
Resolve conflict
rmiccoli Feb 16, 2026
ea700e4
Restore previous maven file
rmiccoli Mar 2, 2026
2a4fa6c
Send explicit registration request to OP
rmiccoli Feb 5, 2026
52a7431
Show the button only if oid-fed profile active
rmiccoli Mar 5, 2026
8ce0ddf
Merge remote-tracking branch 'origin/develop' into issue-1059
rmiccoli Mar 5, 2026
e618f32
Merge branch 'issue-1059' into issue-1062
rmiccoli Mar 5, 2026
06d918a
Change client registration service
rmiccoli Mar 5, 2026
0e022ed
Fix code smells
rmiccoli Mar 5, 2026
1b9de23
Restore previous changes
rmiccoli Mar 5, 2026
5e0528e
Add tests
rmiccoli Mar 10, 2026
da4beda
Fix code smells
rmiccoli Mar 10, 2026
940d004
Use RestTemplateFactory to create RestTemplate
rmiccoli Mar 12, 2026
fb465c7
Merge branch 'develop' into issue-1062
enricovianello Mar 13, 2026
7fc1a47
Fix errors due to merge
enricovianello Mar 13, 2026
44def8c
Change thrown exceptions
rmiccoli Mar 16, 2026
036217f
Add tests
rmiccoli Mar 16, 2026
36dc677
Add client type column to client rp table
rmiccoli Mar 17, 2026
ecf18ad
Bump to a new mitre version
rmiccoli Mar 17, 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
Expand Up @@ -127,8 +127,8 @@ public RegisteredClientDTO saveNewClient(RegisteredClientDTO client) throws Pars
entity.setActive(true);

if (hasRelyingParty(client)) {
ClientRelyingPartyEntity clientRelyingParty =
new ClientRelyingPartyEntity(entity, client.getExpiration(), client.getEntityId());
ClientRelyingPartyEntity clientRelyingParty = new ClientRelyingPartyEntity(entity,
client.getExpiration(), client.getEntityId(), client.getClientType());
entity.setClientRelyingParty(clientRelyingParty);
entity.setRequestObjectSigningAlg(client.getRequestObjectSigningAlgorithm());
}
Expand Down Expand Up @@ -197,7 +197,7 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO cli

if (hasRelyingParty(clientDTO)) {
ClientRelyingPartyEntity clientRelyingParty = new ClientRelyingPartyEntity(newClient,
clientDTO.getExpiration(), clientDTO.getEntityId());
clientDTO.getExpiration(), clientDTO.getEntityId(), clientDTO.getClientType());
newClient.setClientRelyingParty(clientRelyingParty);
newClient.setRequestObjectSigningAlg(clientDTO.getRequestObjectSigningAlgorithm());
newClient.setActive(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@
*/
package it.infn.mw.iam.api.client.registration.service;

import static it.infn.mw.iam.api.client.util.ClientSuppliers.clientNotFound;
import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.ADMINISTRATORS;
import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.ANYONE;
import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.REGISTERED_USERS;
import static java.util.Objects.isNull;
import static java.util.stream.Collectors.toSet;

import java.text.ParseException;
import java.time.Clock;
import java.time.Instant;
import java.util.EnumSet;
import static java.util.Objects.isNull;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import static java.util.stream.Collectors.toSet;

import javax.validation.constraints.NotBlank;

Expand All @@ -50,7 +55,6 @@
import it.infn.mw.iam.api.client.service.ClientConverter;
import it.infn.mw.iam.api.client.service.ClientDefaultsService;
import it.infn.mw.iam.api.client.service.ClientService;
import static it.infn.mw.iam.api.client.util.ClientSuppliers.clientNotFound;
import it.infn.mw.iam.api.common.client.AuthorizationGrantType;
import it.infn.mw.iam.api.common.client.RegisteredClientDTO;
import it.infn.mw.iam.audit.events.account.client.AccountClientOwnerAssigned;
Expand All @@ -60,9 +64,6 @@
import it.infn.mw.iam.audit.events.client.ClientUpdatedEvent;
import it.infn.mw.iam.config.client_registration.ClientRegistrationProperties;
import it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy;
import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.ADMINISTRATORS;
import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.ANYONE;
import static it.infn.mw.iam.config.client_registration.ClientRegistrationProperties.ClientRegistrationAuthorizationPolicy.REGISTERED_USERS;
import it.infn.mw.iam.core.IamTokenService;
import it.infn.mw.iam.core.oauth.scope.matchers.ScopeMatcher;
import it.infn.mw.iam.core.oauth.scope.matchers.ScopeMatcherRegistry;
Expand Down Expand Up @@ -370,8 +371,8 @@ public RegisteredClientDTO registerClient(RegisteredClientDTO request,
}

if (hasRelyingParty(request)) {
ClientRelyingPartyEntity clientRelyingParty =
new ClientRelyingPartyEntity(client, request.getExpiration(), request.getEntityId());
ClientRelyingPartyEntity clientRelyingParty = new ClientRelyingPartyEntity(client,
request.getExpiration(), request.getEntityId(), request.getClientType());
client.setClientRelyingParty(clientRelyingParty);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.URL;
import org.mitre.oauth2.model.ClientRelyingPartyEntity.ClientType;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
Expand Down Expand Up @@ -257,6 +258,11 @@ public class RegisteredClientDTO {
@JsonFormat(shape = JsonFormat.Shape.STRING)
private String entityId;

@JsonView({ClientViews.Limited.class, ClientViews.ClientManagement.class,
ClientViews.NoSecretDynamicRegistration.class, ClientViews.DynamicRegistration.class})
@JsonFormat(shape = JsonFormat.Shape.STRING)
private ClientType clientType;

@JsonView({ClientViews.Full.class, ClientViews.ClientManagement.class,
ClientViews.DynamicRegistration.class})
@Size(max = 65535, groups = {OnClientCreation.class, OnClientUpdate.class})
Expand Down Expand Up @@ -539,6 +545,14 @@ public void setEntityId(String entityId) {
this.entityId = entityId;
}

public ClientType getClientType() {
return clientType;
}

public void setClientType(ClientType clientType) {
this.clientType = clientType;
}

public String getJwk() {
return jwk;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/**
* 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.openid_federation;

import static it.infn.mw.iam.core.oidc.FederationException.invalidClientMetadata;
import static it.infn.mw.iam.core.oidc.FederationException.invalidRequest;
import static it.infn.mw.iam.core.oidc.FederationException.invalidTrustChain;

import java.net.URI;
import java.text.ParseException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.model.ClientRelyingPartyEntity.ClientType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.GrantType;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
import com.nimbusds.openid.connect.sdk.federation.trust.TrustChain;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;

import it.infn.mw.iam.api.client.registration.service.ClientRegistrationService;
import it.infn.mw.iam.api.client.service.ClientService;
import it.infn.mw.iam.api.common.client.AuthorizationGrantType;
import it.infn.mw.iam.api.common.client.OAuthResponseType;
import it.infn.mw.iam.api.common.client.RegisteredClientDTO;
import it.infn.mw.iam.api.common.client.TokenEndpointAuthenticationMethod;
import it.infn.mw.iam.authn.oidc.RestTemplateFactory;
import it.infn.mw.iam.config.oidc.OpenidFederationProperties;
import it.infn.mw.iam.core.oidc.ExplicitRegistrationEntityStatementBuilder;
import it.infn.mw.iam.core.oidc.FederationException;
import it.infn.mw.iam.core.oidc.TrustChainService;

@Service
@Profile("openid-federation")
public class FederatedOpRegistrationService {

public static final Logger LOG = LoggerFactory.getLogger(FederatedOpRegistrationService.class);

private final TrustChainService tcService;
private final ExplicitRegistrationEntityStatementBuilder explRegistrationEsBuilder;
private final ClientRegistrationService clientRegistrationService;
private final ClientService clientService;
private final OpenidFederationProperties oidFedProperties;
private final RestTemplate restTemplate;

@Value("${iam.baseUrl}")
private String iamBaseUrl;

public FederatedOpRegistrationService(TrustChainService tcService,
ExplicitRegistrationEntityStatementBuilder explRegistrationEsBuilder,
ClientRegistrationService clientRegistrationService, ClientService clientService,
OpenidFederationProperties oidFedProperties, RestTemplateFactory restTemplateFactory) {

this.tcService = tcService;
this.explRegistrationEsBuilder = explRegistrationEsBuilder;
this.clientRegistrationService = clientRegistrationService;
this.clientService = clientService;
this.oidFedProperties = oidFedProperties;
this.restTemplate = restTemplateFactory.newRestTemplate();
}

public RegisteredClientDTO registerOp(String issuer, Optional<ClientDetailsEntity> existingClient)
throws JOSEException, ParseException, FederationException {

validateIssuer(issuer);

// 1. Resolve trust chain
TrustChain trustChain = tcService.validateFromEntityId(issuer);

// 2. Select authority_hints
List<String> authorityHints = selectAuthorityHints(trustChain, issuer);

// 3. Build Explicit Registration Entity Statement
String registrationJwt = explRegistrationEsBuilder.build(issuer, authorityHints);

// 4. Discover OP federation registration endpoint
EntityStatement opEc = trustChain.getLeafSelfStatement();
URI regEndpoint = opEc.getClaimsSet().getOPMetadata().getFederationRegistrationEndpointURI();

// 5. POST explicit registration request
String responseJwt = postRegistration(regEndpoint, registrationJwt);
SignedJWT signedResponse = SignedJWT.parse(responseJwt);

// 6. Persist client
RegisteredClientDTO dtoClient = createClientDtoFromOpMetadata(opEc);
dtoClient.setExpiration(trustChain.resolveExpirationTime());
dtoClient.setRequestObjectSigningAlgorithm(signedResponse.getHeader().getAlgorithm());
dtoClient.setClientType(ClientType.EXTERNAL);

RegisteredClientDTO registeredClient =
clientRegistrationService.registerClient(dtoClient, null);

if (existingClient.isPresent()) {
clientService.deleteClient(existingClient.get());
}

return registeredClient;
}

private List<String> selectAuthorityHints(TrustChain trustChain, String issuer)
throws FederationException {

List<String> chainIssuers = trustChain.getSuperiorStatements()
.stream()
.map(es -> es.getClaimsSet().getIssuer().getValue())
.distinct()
.toList();

List<String> configuredHints = oidFedProperties.getEntityConfiguration().getAuthorityHints();

List<String> selected = chainIssuers.stream().filter(configuredHints::contains).toList();

if (selected.isEmpty()) {
throw invalidTrustChain("No valid authority_hints found for OP: " + issuer);
}

return selected;
}

private String postRegistration(URI endpoint, String jwt) throws FederationException {

HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("application", "entity-statement+jwt"));

HttpEntity<String> entity = new HttpEntity<>(jwt, headers);

try {
return restTemplate.postForObject(endpoint, entity, String.class);
} catch (HttpClientErrorException e) {
throw invalidRequest("Federation registration failed: " + e.getResponseBodyAsString(), e);
}
}

private void validateIssuer(String issuer) throws FederationException {
URI uri = URI.create(issuer);
if (!"https".equalsIgnoreCase(uri.getScheme())) {
throw invalidRequest("Issuer must use https");
}
}

private RegisteredClientDTO createClientDtoFromOpMetadata(EntityStatement opEc)
throws FederationException {
RegisteredClientDTO dtoClient = new RegisteredClientDTO();
OIDCProviderMetadata metadata = opEc.getClaimsSet().getOPMetadata();

dtoClient.setClientName("OIDFed OP client");
Set<AuthorizationGrantType> grantTypes = Optional.ofNullable(metadata.getGrantTypes())
.orElse(List.of(GrantType.AUTHORIZATION_CODE))
.stream()
.map(GrantType::getValue)
.map(AuthorizationGrantType::fromGrantType)
.collect(Collectors.toSet());

dtoClient.setGrantTypes(grantTypes);
dtoClient.setRedirectUris(Set.of(iamBaseUrl + "/openid_connect_login"));
Set<String> supportedResponseTypes =
Set.of(ResponseType.CODE.toString(), ResponseType.TOKEN.toString());
if (metadata.getResponseTypes() != null) {
Set<OAuthResponseType> responseTypes = metadata.getResponseTypes()
.stream()
.map(ResponseType::toString)
.filter(supportedResponseTypes::contains)
.map(OAuthResponseType::fromResponseType)
.collect(Collectors.toSet());
if (responseTypes.isEmpty()) {
throw invalidClientMetadata("Unsupported response type");
}
dtoClient.setResponseTypes(responseTypes);
} else {
dtoClient.setResponseTypes(Set.of(OAuthResponseType.CODE));
}
dtoClient.setTokenEndpointAuthMethod(TokenEndpointAuthenticationMethod.client_secret_basic);
if (metadata.getScopes() != null) {
dtoClient.setScope(metadata.getScopes().toStringList().stream().collect(Collectors.toSet()));
} else {
dtoClient.setScope(Set.of("openid"));
}
dtoClient.setEntityId(opEc.getEntityID().getValue());
Optional.ofNullable(metadata.getJWKSetURI())
.ifPresent(uri -> dtoClient.setJwksUri(uri.toASCIIString()));

LOG.debug("Client metadata mapped successfully for OP: {}", dtoClient.getEntityId());
return dtoClient;
}
}
Loading
Loading