View Javadoc
1   /*
2    * Copyright (C) 2023 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.security;
7   
8   import static org.apache.commons.lang3.StringUtils.isNotBlank;
9   
10  import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
11  import jakarta.annotation.Nonnull;
12  import jakarta.annotation.PostConstruct;
13  import java.lang.invoke.MethodHandles;
14  import java.net.URI;
15  import java.net.http.HttpClient;
16  import java.net.http.HttpRequest;
17  import java.net.http.HttpResponse;
18  import java.util.HashMap;
19  import java.util.Iterator;
20  import java.util.Map;
21  import java.util.UUID;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  import org.springframework.beans.factory.annotation.Value;
25  import org.springframework.security.oauth2.client.registration.ClientRegistration;
26  import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
27  import org.springframework.security.oauth2.core.AuthorizationGrantType;
28  import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
29  import org.tailormap.api.persistence.OIDCConfiguration;
30  import org.tailormap.api.repository.OIDCConfigurationRepository;
31  
32  public class OIDCRepository implements ClientRegistrationRepository, Iterable<ClientRegistration> {
33    public static class OIDCRegistrationMetadata {
34      private boolean showForViewer;
35      private UUID image;
36  
37      public boolean getShowForViewer() {
38        return showForViewer;
39      }
40  
41      public UUID getImage() {
42        return image;
43      }
44    }
45  
46    private static final Logger logger =
47        LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
48    private final OIDCConfigurationRepository oidcConfigurationRepository;
49  
50    @Value("${tailormap-api.oidc.name:#{null}}")
51    private String oidcName;
52  
53    @Value("${tailormap-api.oidc.issuer-uri:#{null}}")
54    private String oidcIssuerUri;
55  
56    @Value("${tailormap-api.oidc.client-id:#{null}}")
57    private String oidcClientId;
58  
59    @Value("${tailormap-api.oidc.client-secret:#{null}}")
60    private String oidcClientSecret;
61  
62    @Value("${tailormap-api.oidc.user-name-attribute:#{null}}")
63    private String oidcUserNameAttribute;
64  
65    @Value("${tailormap-api.oidc.show-for-viewer:false}")
66    private boolean oidcShowForViewer;
67  
68    private final Map<String, ClientRegistration> registrations;
69  
70    public OIDCRepository(OIDCConfigurationRepository repository) {
71      oidcConfigurationRepository = repository;
72      registrations = new HashMap<>();
73    }
74  
75    @Override
76    public ClientRegistration findByRegistrationId(String registrationId) {
77      return registrations.get(registrationId);
78    }
79  
80    @Override
81    @Nonnull
82    public Iterator<ClientRegistration> iterator() {
83      return registrations.values().iterator();
84    }
85  
86    public OIDCRegistrationMetadata getMetadataForRegistrationId(String id) {
87      OIDCRegistrationMetadata metadata = new OIDCRegistrationMetadata();
88      if ("static".equals(id)) {
89        metadata.showForViewer = oidcShowForViewer;
90      } else {
91        metadata.showForViewer = true;
92        metadata.image = oidcConfigurationRepository
93            .findById(Long.valueOf(id))
94            .map(OIDCConfiguration::getImage)
95            .orElse(null);
96      }
97  
98      return metadata;
99    }
100 
101   @PostConstruct
102   public void synchronize() {
103     Map<String, ClientRegistration> newMap = new HashMap<>();
104 
105     for (OIDCConfiguration configuration : oidcConfigurationRepository.findAll()) {
106       String id = "%d".formatted(configuration.getId());
107       try (HttpClient httpClient = HttpClient.newBuilder().build()) {
108         HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
109             .uri(new URI(configuration.getIssuerUrl() + "/.well-known/openid-configuration"));
110         HttpResponse<String> response =
111             httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
112 
113         OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(response.body());
114 
115         newMap.put(
116             id,
117             ClientRegistration.withRegistrationId(id)
118                 .clientId(configuration.getClientId())
119                 .clientSecret(configuration.getClientSecret())
120                 .clientName(configuration.getName())
121                 .scope("openid")
122                 .issuerUri(metadata.getIssuer().toString())
123                 .clientAuthenticationMethod(
124                     ClientAuthenticationMethod
125                         .CLIENT_SECRET_BASIC) // TODO: fetch from OIDC metadata
126                 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
127                 .authorizationUri(
128                     metadata.getAuthorizationEndpointURI().toASCIIString())
129                 .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
130                 .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
131                 .providerConfigurationMetadata(metadata.toJSONObject())
132                 .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
133                 .userNameAttributeName(configuration.getUserNameAttribute())
134                 .redirectUri("{baseUrl}/api/oauth2/callback")
135                 .build());
136         if (configuration.getStatus() != null) {
137           configuration.setStatus(null);
138           oidcConfigurationRepository.save(configuration);
139         }
140       } catch (Exception e) {
141         logger.error("Failed to create OIDC client registration for ID {}", id, e);
142         configuration.setStatus(e.toString());
143         oidcConfigurationRepository.save(configuration);
144       }
145     }
146 
147     if (isNotBlank(oidcName) && isNotBlank(oidcIssuerUri) && isNotBlank(oidcClientId)) {
148       try (HttpClient httpClient = HttpClient.newBuilder().build()) {
149         // When copying the URI from some IdP control panels into an .env file, this suffix won't be
150         // stripped by OIDCConfigurationEventHandler.handleBeforeCreateOrSave() so accept both
151         if (!oidcIssuerUri.endsWith("/.well-known/openid-configuration")) {
152           oidcIssuerUri = oidcIssuerUri + "/.well-known/openid-configuration";
153         }
154         HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(new URI(oidcIssuerUri));
155         HttpResponse<String> response =
156             httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
157 
158         OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(response.body());
159         String id = "static";
160 
161         newMap.put(
162             id,
163             ClientRegistration.withRegistrationId(id)
164                 .clientId(oidcClientId)
165                 .clientSecret(oidcClientSecret)
166                 .clientName(oidcName)
167                 .scope("openid")
168                 .issuerUri(metadata.getIssuer().toString())
169                 .clientAuthenticationMethod(
170                     ClientAuthenticationMethod
171                         .CLIENT_SECRET_BASIC) // TODO: fetch from OIDC metadata
172                 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
173                 .authorizationUri(
174                     metadata.getAuthorizationEndpointURI().toASCIIString())
175                 .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
176                 .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
177                 .providerConfigurationMetadata(metadata.toJSONObject())
178                 .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
179                 .userNameAttributeName(oidcUserNameAttribute)
180                 .redirectUri("{baseUrl}/api/oauth2/callback")
181                 .build());
182       } catch (Exception e) {
183         logger.error("Failed to create static OIDC client registration", e);
184       }
185     }
186 
187     registrations.clear();
188     registrations.putAll(newMap);
189   }
190 }