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