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.PostConstruct;
12  import jakarta.validation.constraints.NotNull;
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    public @NotNull Iterator<ClientRegistration> iterator() {
76      return registrations.values().iterator();
77    }
78  
79    public OIDCRegistrationMetadata getMetadataForRegistrationId(String id) {
80      OIDCRegistrationMetadata metadata = new OIDCRegistrationMetadata();
81      if ("static".equals(id)) {
82        metadata.showForViewer = oidcShowForViewer;
83      } else {
84        metadata.showForViewer = true;
85      }
86  
87      return metadata;
88    }
89  
90    @PostConstruct
91    public void synchronize() {
92      Map<String, ClientRegistration> newMap = new HashMap<>();
93  
94      final HttpClient httpClient =
95          HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
96      for (OIDCConfiguration configuration : oidcConfigurationRepository.findAll()) {
97        String id = String.format("%d", configuration.getId());
98        try {
99          HttpRequest.Builder requestBuilder =
100             HttpRequest.newBuilder()
101                 .uri(new URI(configuration.getIssuerUrl() + "/.well-known/openid-configuration"));
102         HttpResponse<String> response =
103             httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
104 
105         OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(response.body());
106 
107         newMap.put(
108             id,
109             ClientRegistration.withRegistrationId(id)
110                 .clientId(configuration.getClientId())
111                 .clientSecret(configuration.getClientSecret())
112                 .clientName(configuration.getName())
113                 .scope("openid")
114                 .issuerUri(metadata.getIssuer().toString())
115                 .clientAuthenticationMethod(
116                     ClientAuthenticationMethod
117                         .CLIENT_SECRET_BASIC) // TODO: fetch from OIDC metadata
118                 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
119                 .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
120                 .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
121                 .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
122                 .providerConfigurationMetadata(metadata.toJSONObject())
123                 .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
124                 .userNameAttributeName(configuration.getUserNameAttribute())
125                 .redirectUri("{baseUrl}/api/oauth2/callback")
126                 .build());
127         if (configuration.getStatus() != null) {
128           configuration.setStatus(null);
129           oidcConfigurationRepository.save(configuration);
130         }
131       } catch (Exception e) {
132         logger.error("Failed to create OIDC client registration for ID " + id, e);
133         configuration.setStatus(e.toString());
134         oidcConfigurationRepository.save(configuration);
135       }
136     }
137 
138     if (isNotBlank(oidcName) && isNotBlank(oidcIssuerUri) && isNotBlank(oidcClientId)) {
139       try {
140         // When copying the URI from some IdP control panels into an .env file, this suffix won't be
141         // stripped by OIDCConfigurationEventHandler.handleBeforeCreateOrSave() so accept both
142         if (!oidcIssuerUri.endsWith("/.well-known/openid-configuration")) {
143           oidcIssuerUri = oidcIssuerUri + "/.well-known/openid-configuration";
144         }
145         HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(new URI(oidcIssuerUri));
146         HttpResponse<String> response =
147             httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
148 
149         OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(response.body());
150         String id = "static";
151 
152         newMap.put(
153             id,
154             ClientRegistration.withRegistrationId(id)
155                 .clientId(oidcClientId)
156                 .clientSecret(oidcClientSecret)
157                 .clientName(oidcName)
158                 .scope("openid")
159                 .issuerUri(metadata.getIssuer().toString())
160                 .clientAuthenticationMethod(
161                     ClientAuthenticationMethod
162                         .CLIENT_SECRET_BASIC) // TODO: fetch from OIDC metadata
163                 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
164                 .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
165                 .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
166                 .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
167                 .providerConfigurationMetadata(metadata.toJSONObject())
168                 .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
169                 .userNameAttributeName(oidcUserNameAttribute)
170                 .redirectUri("{baseUrl}/api/oauth2/callback")
171                 .build());
172       } catch (Exception e) {
173         logger.error("Failed to create static OIDC client registration", e);
174       }
175     }
176 
177     registrations.clear();
178     registrations.putAll(newMap);
179   }
180 }