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     final HttpClient httpClient = HttpClient.newBuilder()
106         .followRedirects(HttpClient.Redirect.NORMAL)
107         .build();
108     for (OIDCConfiguration configuration : oidcConfigurationRepository.findAll()) {
109       String id = String.format("%d", configuration.getId());
110       try {
111         HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
112             .uri(new URI(configuration.getIssuerUrl() + "/.well-known/openid-configuration"));
113         HttpResponse<String> response =
114             httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
115 
116         OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(response.body());
117 
118         newMap.put(
119             id,
120             ClientRegistration.withRegistrationId(id)
121                 .clientId(configuration.getClientId())
122                 .clientSecret(configuration.getClientSecret())
123                 .clientName(configuration.getName())
124                 .scope("openid")
125                 .issuerUri(metadata.getIssuer().toString())
126                 .clientAuthenticationMethod(
127                     ClientAuthenticationMethod
128                         .CLIENT_SECRET_BASIC) // TODO: fetch from OIDC metadata
129                 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
130                 .authorizationUri(
131                     metadata.getAuthorizationEndpointURI().toASCIIString())
132                 .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
133                 .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
134                 .providerConfigurationMetadata(metadata.toJSONObject())
135                 .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
136                 .userNameAttributeName(configuration.getUserNameAttribute())
137                 .redirectUri("{baseUrl}/api/oauth2/callback")
138                 .build());
139         if (configuration.getStatus() != null) {
140           configuration.setStatus(null);
141           oidcConfigurationRepository.save(configuration);
142         }
143       } catch (Exception e) {
144         logger.error("Failed to create OIDC client registration for ID {}", id, e);
145         configuration.setStatus(e.toString());
146         oidcConfigurationRepository.save(configuration);
147       }
148     }
149 
150     if (isNotBlank(oidcName) && isNotBlank(oidcIssuerUri) && isNotBlank(oidcClientId)) {
151       try {
152         // When copying the URI from some IdP control panels into an .env file, this suffix won't be
153         // stripped by OIDCConfigurationEventHandler.handleBeforeCreateOrSave() so accept both
154         if (!oidcIssuerUri.endsWith("/.well-known/openid-configuration")) {
155           oidcIssuerUri = oidcIssuerUri + "/.well-known/openid-configuration";
156         }
157         HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(new URI(oidcIssuerUri));
158         HttpResponse<String> response =
159             httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
160 
161         OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(response.body());
162         String id = "static";
163 
164         newMap.put(
165             id,
166             ClientRegistration.withRegistrationId(id)
167                 .clientId(oidcClientId)
168                 .clientSecret(oidcClientSecret)
169                 .clientName(oidcName)
170                 .scope("openid")
171                 .issuerUri(metadata.getIssuer().toString())
172                 .clientAuthenticationMethod(
173                     ClientAuthenticationMethod
174                         .CLIENT_SECRET_BASIC) // TODO: fetch from OIDC metadata
175                 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
176                 .authorizationUri(
177                     metadata.getAuthorizationEndpointURI().toASCIIString())
178                 .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
179                 .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
180                 .providerConfigurationMetadata(metadata.toJSONObject())
181                 .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
182                 .userNameAttributeName(oidcUserNameAttribute)
183                 .redirectUri("{baseUrl}/api/oauth2/callback")
184                 .build());
185       } catch (Exception e) {
186         logger.error("Failed to create static OIDC client registration", e);
187       }
188     }
189 
190     registrations.clear();
191     registrations.putAll(newMap);
192   }
193 }