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.Map;
23  
24  import javax.annotation.Nullable;
25  
26  import org.opensaml.xmlsec.signature.support.SignatureConstants;
27  
28  import com.fasterxml.jackson.annotation.JsonCreator;
29  import com.fasterxml.jackson.annotation.JsonProperty;
30  import com.fasterxml.jackson.core.JsonProcessingException;
31  import com.google.common.collect.ImmutableMap;
32  import com.google.common.collect.ImmutableMap.Builder;
33  
34  import com.linecorp.armeria.server.saml.SamlBindingProtocol;
35  import com.linecorp.armeria.server.saml.SamlEndpoint;
36  import com.linecorp.armeria.server.saml.SamlNameIdFormat;
37  import com.linecorp.centraldogma.internal.Jackson;
38  
39  /**
40   * Properties which are used to configure SAML authentication for Central Dogma server.
41   * A user can specify them as the authentication property in the {@code dogma.json} as follows:
42   * <pre>{@code
43   * "authentication": {
44   *     "factoryClassName": "com.linecorp.centraldogma.server.auth.saml.SamlAuthProviderFactory",
45   *     "properties": {
46   *         "entityId": "...the service provider ID...",
47   *         "hostname": "dogma-example.linecorp.com",
48   *         "signingKey": "...the name of signing key (optional)...",
49   *         "encryptionKey": "...the name of encryption key (optional)...",
50   *         "keyStore": {
51   *             "type": "...the type of the keystore (optional)...",
52   *             "path": "...the path where keystore file exists...",
53   *             "password": "...the password of the keystore (optional)...",
54   *             "keyPasswords": {
55   *                 "signing": "...the password of the signing key...",
56   *                 "encryption": "...the password of the encryption key..."
57   *             },
58   *             "signatureAlgorithm": "...the signature algorithm for signing and encryption (optional)..."
59   *         },
60   *         "idp": {
61   *             "entityId": "...the identity provider ID...",
62   *             "uri": "https://idp-example.linecorp.com/saml/sso",
63   *             "binding": "HTTP_POST or HTTP_REDIRECT (optional)",
64   *             "signingKey": "...the name of signing certificate (optional)...",
65   *             "encryptionKey": "...the name of encryption certificate (optional)...",
66   *             "subjectLoginNameIdFormat":
67   *                  "...the name ID format of a subject which holds a login name (optional)...",
68   *             "attributeLoginName": "...the attribute name which holds a login name (optional)..."
69   *         }
70   *     }
71   * }
72   * }</pre>
73   */
74  final class SamlAuthProperties {
75      /**
76       * A default key name for signing.
77       */
78      private static final String DEFAULT_SIGNING_KEY = "signing";
79  
80      /**
81       * A default key name for encryption.
82       */
83      private static final String DEFAULT_ENCRYPTION_KEY = "encryption";
84  
85      /**
86       * An ID of this service provider.
87       */
88      private final String entityId;
89  
90      /**
91       * A hostname of this service provider.
92       */
93      private final String hostname;
94  
95      /**
96       * A key name which is used for signing. The default name is {@value DEFAULT_SIGNING_KEY}.
97       */
98      private final String signingKey;
99  
100     /**
101      * A key name which is used for encryption. The default name is {@value DEFAULT_ENCRYPTION_KEY}.
102      */
103     private final String encryptionKey;
104 
105     /**
106      * A configuration for the keystore.
107      */
108     private final KeyStore keyStore;
109 
110     /**
111      * An identity provider configuration. A single identity provider is supported.
112      */
113     private final Idp idp;
114 
115     @JsonCreator
116     SamlAuthProperties(
117             @JsonProperty("entityId") String entityId,
118             @JsonProperty("hostname") String hostname,
119             @JsonProperty("signingKey") @Nullable String signingKey,
120             @JsonProperty("encryptionKey") @Nullable String encryptionKey,
121             @JsonProperty("keyStore") KeyStore keyStore,
122             @JsonProperty("idp") Idp idp) {
123         this.entityId = requireNonNull(entityId, "entityId");
124         this.hostname = requireNonNull(hostname, "hostname");
125         this.signingKey = firstNonNull(signingKey, DEFAULT_SIGNING_KEY);
126         this.encryptionKey = firstNonNull(encryptionKey, DEFAULT_ENCRYPTION_KEY);
127         this.keyStore = requireNonNull(keyStore, "keyStore");
128         this.idp = requireNonNull(idp, "idp");
129     }
130 
131     @JsonProperty
132     public String entityId() {
133         return entityId;
134     }
135 
136     @JsonProperty
137     public String hostname() {
138         return hostname;
139     }
140 
141     @JsonProperty
142     public String signingKey() {
143         return signingKey;
144     }
145 
146     @JsonProperty
147     public String encryptionKey() {
148         return encryptionKey;
149     }
150 
151     @JsonProperty
152     public KeyStore keyStore() {
153         return keyStore;
154     }
155 
156     @JsonProperty
157     public Idp idp() {
158         return idp;
159     }
160 
161     @Override
162     public String toString() {
163         try {
164             return Jackson.writeValueAsPrettyString(this);
165         } catch (JsonProcessingException e) {
166             throw new IllegalStateException(e);
167         }
168     }
169 
170     static class KeyStore {
171         /**
172          * A default signature algorithm.
173          */
174         private static final String DEFAULT_SIGNATURE_ALGORITHM = SignatureConstants.ALGO_ID_SIGNATURE_RSA;
175 
176         /**
177          * A type of the keystore. The default value is retrieved from
178          * {@code java.security.KeyStore.getDefaultType()}.
179          */
180         private final String type;
181 
182         /**
183          * A path of the keystore.
184          */
185         private final String path;
186 
187         /**
188          * A password of the keystore. The empty string is used by default.
189          */
190         @Nullable
191         private final String password;
192 
193         /**
194          * A map of the key name and its password.
195          */
196         @Nullable
197         private final Map<String, String> keyPasswords;
198 
199         /**
200          * A signature algorithm for signing and encryption. The default algorithm is
201          * {@value DEFAULT_SIGNATURE_ALGORITHM}.
202          *
203          * @see SignatureConstants for more information about the signature algorithm
204          */
205         private final String signatureAlgorithm;
206 
207         @JsonCreator
208         KeyStore(@JsonProperty("type") @Nullable String type,
209                  @JsonProperty("path") String path,
210                  @JsonProperty("password") @Nullable String password,
211                  @JsonProperty("keyPasswords") @Nullable Map<String, String> keyPasswords,
212                  @JsonProperty("signatureAlgorithm") @Nullable String signatureAlgorithm) {
213             this.type = firstNonNull(type, java.security.KeyStore.getDefaultType());
214             this.path = requireNonNull(path, "path");
215             this.password = password;
216             this.keyPasswords = keyPasswords;
217             this.signatureAlgorithm = firstNonNull(signatureAlgorithm, DEFAULT_SIGNATURE_ALGORITHM);
218         }
219 
220         @JsonProperty
221         public String type() {
222             return type;
223         }
224 
225         @JsonProperty
226         public String path() {
227             return path;
228         }
229 
230         @Nullable
231         @JsonProperty
232         public String password() {
233             return convertValue(password, "keyStore.password");
234         }
235 
236         @JsonProperty
237         public Map<String, String> keyPasswords() {
238             return sanitizePasswords(keyPasswords);
239         }
240 
241         private static Map<String, String> sanitizePasswords(@Nullable Map<String, String> keyPasswords) {
242             if (keyPasswords == null) {
243                 return ImmutableMap.of();
244             }
245             final ImmutableMap.Builder<String, String> builder = new Builder<>();
246             keyPasswords.forEach((key, password) -> builder.put(key, firstNonNull(
247                     convertValue(password, "keyStore.keyPasswords"), "")));
248             return builder.build();
249         }
250 
251         @JsonProperty
252         public String signatureAlgorithm() {
253             return signatureAlgorithm;
254         }
255     }
256 
257     static class Idp {
258         /**
259          * An ID of the identity provider.
260          */
261         private final String entityId;
262 
263         /**
264          * A location of the single sign-on service.
265          */
266         private final String uri;
267 
268         /**
269          * A name of a {@link SamlBindingProtocol}. The default name is the
270          * {@link SamlBindingProtocol#HTTP_POST}.
271          */
272         private final SamlBindingProtocol binding;
273 
274         /**
275          * A certificate name which is used for signing. The default name is the {@link #entityId()}.
276          */
277         private final String signingKey;
278 
279         /**
280          * A certificate name which is used for encryption. The default name is the {@link #entityId()}.
281          */
282         private final String encryptionKey;
283 
284         /**
285          * A name ID format of a subject which holds a login name of an authenticated user. If both
286          * {@code subjectLoginNameIdFormat} and {@code attributeLoginName} are {@code null},
287          * {@code urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress} is set by default.
288          */
289         @Nullable
290         private final String subjectLoginNameIdFormat;
291 
292         /**
293          * An attribute name which holds a login name of an authenticated user.
294          */
295         @Nullable
296         private final String attributeLoginName;
297 
298         @JsonCreator
299         Idp(@JsonProperty("entityId") String entityId,
300             @JsonProperty("uri") String uri,
301             @JsonProperty("binding") @Nullable String binding,
302             @JsonProperty("signingKey") @Nullable String signingKey,
303             @JsonProperty("encryptionKey") @Nullable String encryptionKey,
304             @JsonProperty("subjectLoginNameIdFormat") @Nullable String subjectLoginNameIdFormat,
305             @JsonProperty("attributeLoginName") @Nullable String attributeLoginName) {
306             this.entityId = requireNonNull(entityId, "entityId");
307             this.uri = requireNonNull(uri, "uri");
308             this.binding = binding != null ? SamlBindingProtocol.valueOf(binding)
309                                            : SamlBindingProtocol.HTTP_POST;
310             this.signingKey = firstNonNull(signingKey, entityId);
311             this.encryptionKey = firstNonNull(encryptionKey, entityId);
312 
313             if (subjectLoginNameIdFormat == null && attributeLoginName == null) {
314                 this.subjectLoginNameIdFormat = SamlNameIdFormat.EMAIL.urn();
315                 this.attributeLoginName = null;
316             } else {
317                 this.subjectLoginNameIdFormat = subjectLoginNameIdFormat;
318                 this.attributeLoginName = attributeLoginName;
319             }
320         }
321 
322         @JsonProperty
323         public String entityId() {
324             return entityId;
325         }
326 
327         @JsonProperty
328         public String uri() {
329             return uri;
330         }
331 
332         @JsonProperty
333         public String binding() {
334             return binding.name();
335         }
336 
337         @JsonProperty
338         public String signingKey() {
339             return signingKey;
340         }
341 
342         @JsonProperty
343         public String encryptionKey() {
344             return encryptionKey;
345         }
346 
347         @Nullable
348         @JsonProperty
349         public String subjectLoginNameIdFormat() {
350             return subjectLoginNameIdFormat;
351         }
352 
353         @Nullable
354         @JsonProperty
355         public String attributeLoginName() {
356             return attributeLoginName;
357         }
358 
359         public SamlEndpoint endpoint() {
360             switch (binding) {
361                 case HTTP_POST:
362                     return SamlEndpoint.ofHttpPost(uri);
363                 case HTTP_REDIRECT:
364                     return SamlEndpoint.ofHttpRedirect(uri);
365                 default:
366                     throw new IllegalStateException("Failed to get an endpoint of the IdP: " + entityId);
367             }
368         }
369     }
370 }