1   /*
2    * Copyright 2018 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package com.linecorp.centraldogma.server.auth.saml;
17  
18  import static com.google.common.base.MoreObjects.firstNonNull;
19  import static com.linecorp.centraldogma.server.CentralDogmaConfig.convertValue;
20  import static java.util.Objects.requireNonNull;
21  
22  import java.util.List;
23  import java.util.Map;
24  
25  import javax.annotation.Nullable;
26  
27  import org.opensaml.xmlsec.signature.support.SignatureConstants;
28  
29  import com.fasterxml.jackson.annotation.JsonCreator;
30  import com.fasterxml.jackson.annotation.JsonProperty;
31  import com.fasterxml.jackson.core.JsonProcessingException;
32  import com.google.common.collect.ImmutableMap;
33  import com.google.common.collect.ImmutableMap.Builder;
34  
35  import com.linecorp.armeria.server.saml.SamlBindingProtocol;
36  import com.linecorp.armeria.server.saml.SamlEndpoint;
37  import com.linecorp.armeria.server.saml.SamlNameIdFormat;
38  import com.linecorp.centraldogma.internal.Jackson;
39  
40  /**
41   * Properties which are used to configure SAML authentication for Central Dogma server.
42   * A user can specify them as the authentication property in the {@code dogma.json} as follows:
43   * <pre>{@code
44   * "authentication": {
45   *     "factoryClassName": "com.linecorp.centraldogma.server.auth.saml.SamlAuthProviderFactory",
46   *     "properties": {
47   *         "entityId": "...the service provider ID...",
48   *         "hostname": "dogma-example.linecorp.com",
49   *         "signingKey": "...the name of signing key (optional)...",
50   *         "encryptionKey": "...the name of encryption key (optional)...",
51   *         "keyStore": {
52   *             "type": "...the type of the keystore (optional)...",
53   *             "path": "...the path where keystore file exists...",
54   *             "password": "...the password of the keystore (optional)...",
55   *             "keyPasswords": {
56   *                 "signing": "...the password of the signing key...",
57   *                 "encryption": "...the password of the encryption key..."
58   *             },
59   *             "signatureAlgorithm": "...the signature algorithm for signing and encryption (optional)..."
60   *         },
61   *         // Specify when your server uses different uri from the recipient of the assertion that
62   *         // the IdP sends. For example, if your server is behind a proxy, you need to specify
63   *         // the uri of the proxy.
64   *         "acs": {
65   *             "endpoints": [{
66   *                 "uri": "https://dogma-example.linecorp.com/saml/acs/post",
67   *                 "binding": "HTTP_POST"
68   *             }, {
69   *                 "uri": "https://dogma-example.linecorp.com/saml/acs/redirect",
70   *                 "binding": "HTTP_REDIRECT"
71   *             }]
72   *         },
73   *         "idp": {
74   *             "entityId": "...the identity provider ID...",
75   *             "uri": "https://idp-example.linecorp.com/saml/sso",
76   *             "binding": "HTTP_POST or HTTP_REDIRECT (optional)",
77   *             "signingKey": "...the name of signing certificate (optional)...",
78   *             "encryptionKey": "...the name of encryption certificate (optional)...",
79   *             "subjectLoginNameIdFormat":
80   *                  "...the name ID format of a subject which holds a login name (optional)...",
81   *             "attributeLoginName": "...the attribute name which holds a login name (optional)..."
82   *         }
83   *     }
84   * }
85   * }</pre>
86   */
87  final class SamlAuthProperties {
88      /**
89       * A default key name for signing.
90       */
91      private static final String DEFAULT_SIGNING_KEY = "signing";
92  
93      /**
94       * A default key name for encryption.
95       */
96      private static final String DEFAULT_ENCRYPTION_KEY = "encryption";
97  
98      /**
99       * An ID of this service provider.
100      */
101     private final String entityId;
102 
103     /**
104      * A hostname of this service provider.
105      */
106     private final String hostname;
107 
108     /**
109      * A key name which is used for signing. The default name is {@value DEFAULT_SIGNING_KEY}.
110      */
111     private final String signingKey;
112 
113     /**
114      * A key name which is used for encryption. The default name is {@value DEFAULT_ENCRYPTION_KEY}.
115      */
116     private final String encryptionKey;
117 
118     /**
119      * A configuration for the keystore.
120      */
121     private final KeyStore keyStore;
122 
123     /**
124      * {@link SamlEndpoint}s of the assertion consumer service.
125      */
126     @Nullable
127     private final Acs acs;
128 
129     /**
130      * An identity provider configuration. A single identity provider is supported.
131      */
132     private final Idp idp;
133 
134     @JsonCreator
135     SamlAuthProperties(
136             @JsonProperty("entityId") String entityId,
137             @JsonProperty("hostname") String hostname,
138             @JsonProperty("signingKey") @Nullable String signingKey,
139             @JsonProperty("encryptionKey") @Nullable String encryptionKey,
140             @JsonProperty("keyStore") KeyStore keyStore,
141             @JsonProperty("acs") @Nullable Acs acs,
142             @JsonProperty("idp") Idp idp) {
143         this.entityId = requireNonNull(entityId, "entityId");
144         this.hostname = requireNonNull(hostname, "hostname");
145         this.signingKey = firstNonNull(signingKey, DEFAULT_SIGNING_KEY);
146         this.encryptionKey = firstNonNull(encryptionKey, DEFAULT_ENCRYPTION_KEY);
147         this.keyStore = requireNonNull(keyStore, "keyStore");
148         this.acs = acs;
149         this.idp = requireNonNull(idp, "idp");
150     }
151 
152     @JsonProperty
153     String entityId() {
154         return entityId;
155     }
156 
157     @JsonProperty
158     String hostname() {
159         return hostname;
160     }
161 
162     @JsonProperty
163     String signingKey() {
164         return signingKey;
165     }
166 
167     @JsonProperty
168     String encryptionKey() {
169         return encryptionKey;
170     }
171 
172     @JsonProperty
173     KeyStore keyStore() {
174         return keyStore;
175     }
176 
177     @Nullable
178     @JsonProperty
179     Acs acs() {
180         return acs;
181     }
182 
183     @JsonProperty
184     Idp idp() {
185         return idp;
186     }
187 
188     @Override
189     public String toString() {
190         try {
191             return Jackson.writeValueAsPrettyString(this);
192         } catch (JsonProcessingException e) {
193             throw new IllegalStateException(e);
194         }
195     }
196 
197     static class KeyStore {
198         /**
199          * A default signature algorithm.
200          */
201         private static final String DEFAULT_SIGNATURE_ALGORITHM = SignatureConstants.ALGO_ID_SIGNATURE_RSA;
202 
203         /**
204          * A type of the keystore. The default value is retrieved from
205          * {@code java.security.KeyStore.getDefaultType()}.
206          */
207         private final String type;
208 
209         /**
210          * A path of the keystore.
211          */
212         private final String path;
213 
214         /**
215          * A password of the keystore. The empty string is used by default.
216          */
217         @Nullable
218         private final String password;
219 
220         /**
221          * A map of the key name and its password.
222          */
223         @Nullable
224         private final Map<String, String> keyPasswords;
225 
226         /**
227          * A signature algorithm for signing and encryption. The default algorithm is
228          * {@value DEFAULT_SIGNATURE_ALGORITHM}.
229          *
230          * @see SignatureConstants for more information about the signature algorithm
231          */
232         private final String signatureAlgorithm;
233 
234         @JsonCreator
235         KeyStore(@JsonProperty("type") @Nullable String type,
236                  @JsonProperty("path") String path,
237                  @JsonProperty("password") @Nullable String password,
238                  @JsonProperty("keyPasswords") @Nullable Map<String, String> keyPasswords,
239                  @JsonProperty("signatureAlgorithm") @Nullable String signatureAlgorithm) {
240             this.type = firstNonNull(type, java.security.KeyStore.getDefaultType());
241             this.path = requireNonNull(path, "path");
242             this.password = password;
243             this.keyPasswords = keyPasswords;
244             this.signatureAlgorithm = firstNonNull(signatureAlgorithm, DEFAULT_SIGNATURE_ALGORITHM);
245         }
246 
247         @JsonProperty
248         String type() {
249             return type;
250         }
251 
252         @JsonProperty
253         String path() {
254             return path;
255         }
256 
257         @Nullable
258         @JsonProperty
259         String password() {
260             return convertValue(password, "keyStore.password");
261         }
262 
263         @JsonProperty
264         Map<String, String> keyPasswords() {
265             return sanitizePasswords(keyPasswords);
266         }
267 
268         private static Map<String, String> sanitizePasswords(@Nullable Map<String, String> keyPasswords) {
269             if (keyPasswords == null) {
270                 return ImmutableMap.of();
271             }
272             final ImmutableMap.Builder<String, String> builder = new Builder<>();
273             keyPasswords.forEach((key, password) -> builder.put(key, firstNonNull(
274                     convertValue(password, "keyStore.keyPasswords"), "")));
275             return builder.build();
276         }
277 
278         @JsonProperty
279         String signatureAlgorithm() {
280             return signatureAlgorithm;
281         }
282     }
283 
284     static class Acs {
285 
286         private final List<SamlEndpoint> endpoints;
287 
288         @JsonCreator
289         Acs(@JsonProperty("endpoints") List<SamlEndpoint> endpoints) {
290             this.endpoints = requireNonNull(endpoints, "endpoints");
291         }
292 
293         @JsonProperty
294         List<SamlEndpoint> endpoints() {
295             return endpoints;
296         }
297     }
298 
299     static class Idp {
300         /**
301          * An ID of the identity provider.
302          */
303         private final String entityId;
304 
305         /**
306          * A location of the single sign-on service.
307          */
308         private final String uri;
309 
310         /**
311          * A name of a {@link SamlBindingProtocol}. The default name is the
312          * {@link SamlBindingProtocol#HTTP_POST}.
313          */
314         private final SamlBindingProtocol binding;
315 
316         /**
317          * A certificate name which is used for signing. The default name is the {@link #entityId()}.
318          */
319         private final String signingKey;
320 
321         /**
322          * A certificate name which is used for encryption. The default name is the {@link #entityId()}.
323          */
324         private final String encryptionKey;
325 
326         /**
327          * A name ID format of a subject which holds a login name of an authenticated user. If both
328          * {@code subjectLoginNameIdFormat} and {@code attributeLoginName} are {@code null},
329          * {@code urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress} is set by default.
330          */
331         @Nullable
332         private final String subjectLoginNameIdFormat;
333 
334         /**
335          * An attribute name which holds a login name of an authenticated user.
336          */
337         @Nullable
338         private final String attributeLoginName;
339 
340         @JsonCreator
341         Idp(@JsonProperty("entityId") String entityId,
342             @JsonProperty("uri") String uri,
343             @JsonProperty("binding") @Nullable String binding,
344             @JsonProperty("signingKey") @Nullable String signingKey,
345             @JsonProperty("encryptionKey") @Nullable String encryptionKey,
346             @JsonProperty("subjectLoginNameIdFormat") @Nullable String subjectLoginNameIdFormat,
347             @JsonProperty("attributeLoginName") @Nullable String attributeLoginName) {
348             this.entityId = requireNonNull(entityId, "entityId");
349             this.uri = requireNonNull(uri, "uri");
350             this.binding = binding != null ? SamlBindingProtocol.valueOf(binding)
351                                            : SamlBindingProtocol.HTTP_POST;
352             this.signingKey = firstNonNull(signingKey, entityId);
353             this.encryptionKey = firstNonNull(encryptionKey, entityId);
354 
355             if (subjectLoginNameIdFormat == null && attributeLoginName == null) {
356                 this.subjectLoginNameIdFormat = SamlNameIdFormat.EMAIL.urn();
357                 this.attributeLoginName = null;
358             } else {
359                 this.subjectLoginNameIdFormat = subjectLoginNameIdFormat;
360                 this.attributeLoginName = attributeLoginName;
361             }
362         }
363 
364         @JsonProperty
365         String entityId() {
366             return entityId;
367         }
368 
369         @JsonProperty
370         String uri() {
371             return uri;
372         }
373 
374         @JsonProperty
375         String binding() {
376             return binding.name();
377         }
378 
379         @JsonProperty
380         String signingKey() {
381             return signingKey;
382         }
383 
384         @JsonProperty
385         String encryptionKey() {
386             return encryptionKey;
387         }
388 
389         @Nullable
390         @JsonProperty
391         String subjectLoginNameIdFormat() {
392             return subjectLoginNameIdFormat;
393         }
394 
395         @Nullable
396         @JsonProperty
397         String attributeLoginName() {
398             return attributeLoginName;
399         }
400 
401         SamlEndpoint endpoint() {
402             switch (binding) {
403                 case HTTP_POST:
404                     return SamlEndpoint.ofHttpPost(uri);
405                 case HTTP_REDIRECT:
406                     return SamlEndpoint.ofHttpRedirect(uri);
407                 default:
408                     throw new IllegalStateException("Failed to get an endpoint of the IdP: " + entityId);
409             }
410         }
411     }
412 }