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