View Javadoc
1   /*
2    * Copyright 2024-2026 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://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,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.spring.boot.autoconfigure.security.authentication;
18  
19  import static org.springframework.util.ObjectUtils.isEmpty;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.stream.Collectors;
27  import java.util.stream.Stream;
28  import lombok.Data;
29  import org.springframework.boot.context.properties.ConfigurationProperties;
30  
31  /**
32   * The authentication properties.
33   *
34   * @author Christian Bremer
35   */
36  @ConfigurationProperties(prefix = "bremersee.authentication")
37  @Data
38  public class AuthenticationProperties {
39  
40    /**
41     * The remember-me properties.
42     */
43    private RememberMeProperties rememberMe = new RememberMeProperties();
44  
45    /**
46     * The jwt converter properties.
47     */
48    private JwtConverterProperties jwtConverter = new JwtConverterProperties();
49  
50    /**
51     * The ldaptive properties.
52     */
53    private LdaptiveProperties ldaptive = new LdaptiveProperties();
54  
55    /**
56     * The actuator properties.
57     */
58    private ActuatorProperties actuator = new ActuatorProperties();
59  
60    /**
61     * Instantiates new authentication properties.
62     */
63    public AuthenticationProperties() {
64      super();
65    }
66  
67    /**
68     * The remember-me properties.
69     *
70     * @author Christian Bremer
71     */
72    @Data
73    public static class RememberMeProperties {
74  
75      /**
76       * The key.
77       */
78      private String key;
79  
80      /**
81       * Specifies whether remember-me is always activated.
82       */
83      private Boolean alwaysRemember;
84  
85      /**
86       * The cookie name.
87       */
88      private String cookieName;
89  
90      /**
91       * The cookie domain.
92       */
93      private String cookieDomain;
94  
95      /**
96       * Specifies whether to use secure cookie.
97       */
98      private Boolean useSecureCookie;
99  
100     /**
101      * The parameter name (default remember-me).
102      */
103     private String parameterName;
104 
105     /**
106      * The token validity in seconds (default two weeks).
107      */
108     private Integer tokenValiditySeconds;
109 
110     /**
111      * Instantiates new remember-me properties.
112      */
113     public RememberMeProperties() {
114       super();
115     }
116   }
117 
118   /**
119    * The jwt converter properties.
120    */
121   @Data
122   public static class JwtConverterProperties {
123 
124     /**
125      * The json path to the username.
126      */
127     private String nameJsonPath = "$.sub"; // keycloak: $.preferred_username
128 
129     /**
130      * The json path to the first name.
131      */
132     private String firstNameJsonPath = "$.given_name";
133 
134     /**
135      * The json path to the last name.
136      */
137     private String lastNameJsonPath = "$.family_name";
138 
139     /**
140      * The json path to the email.
141      */
142     private String emailJsonPath = "$.email";
143 
144     /**
145      * The json path to the roles.
146      */
147     private String rolesJsonPath = "$.scope"; // keycloak: $.realm_access.roles
148 
149     /**
150      * Specifies whether the roles are represented as a json array or as a list separated by
151      * {@link #getRolesValueSeparator()}.
152      */
153     private boolean rolesValueList = false; // keycloak: true
154 
155     /**
156      * The roles separator to use if {@link #isRolesValueList()} is set to {@code false}.
157      */
158     private String rolesValueSeparator = " ";
159 
160     /**
161      * The default roles.
162      */
163     private List<String> defaultRoles = new ArrayList<>();
164 
165     /**
166      * The role mappings.
167      */
168     private List<SimpleMapping> roleMapping = new ArrayList<>();
169 
170     /**
171      * The role prefix (like 'ROLE_' or 'SCOPE_').
172      */
173     private String rolePrefix = "SCOPE_"; // keycloak: ROLE_
174 
175     /**
176      * The role case transformation.
177      */
178     private CaseTransformation roleCaseTransformation;
179 
180     /**
181      * The string replacements for roles.
182      */
183     private List<StringReplacement> roleStringReplacements;
184 
185     /**
186      * The json path to the groups.
187      */
188     private String groupsJsonPath = "$.groups";
189 
190     /**
191      * Specifies whether the groups are represented as a json array or as a list separated by
192      * {@link #getGroupsValueSeparator()}.
193      */
194     private boolean groupsValueList = false; // keycloak: true
195 
196     /**
197      * The groups separator to use if {@link #isGroupsValueList()} is set to {@code false}.
198      */
199     private String groupsValueSeparator = " ";
200 
201     /**
202      * The default groups.
203      */
204     private List<String> defaultGroups = new ArrayList<>();
205 
206     /**
207      * The group mappings.
208      */
209     private List<SimpleMapping> groupMapping = new ArrayList<>();
210 
211     /**
212      * The group prefix (like 'GROUP_').
213      */
214     private String groupPrefix = "";
215 
216     /**
217      * The group case transformation.
218      */
219     private CaseTransformation groupCaseTransformation;
220 
221     /**
222      * The string replacements for groups.
223      */
224     private List<StringReplacement> groupStringReplacements;
225 
226     /**
227      * Instantiates new jwt converter properties.
228      */
229     public JwtConverterProperties() {
230       super();
231     }
232 
233     /**
234      * To role mappings.
235      *
236      * @return the map
237      */
238     public Map<String, String> toRoleMappings() {
239       return Stream.ofNullable(getRoleMapping())
240           .flatMap(Collection::stream)
241           .collect(Collectors.toMap(
242               SimpleMapping::getSource,
243               SimpleMapping::getTarget,
244               (first, second) -> first,
245               LinkedHashMap::new));
246     }
247 
248     /**
249      * To group mappings.
250      *
251      * @return the map
252      */
253     public Map<String, String> toGroupMappings() {
254       return Stream.ofNullable(getGroupMapping())
255           .flatMap(Collection::stream)
256           .collect(Collectors.toMap(
257               SimpleMapping::getSource,
258               SimpleMapping::getTarget,
259               (first, second) -> first,
260               LinkedHashMap::new));
261     }
262 
263     /**
264      * To role string replacements.
265      *
266      * @return the map
267      */
268     public Map<String, String> toRoleStringReplacements() {
269       return Stream.ofNullable(getRoleStringReplacements())
270           .flatMap(Collection::stream)
271           .collect(Collectors.toMap(
272               StringReplacement::getRegex,
273               StringReplacement::getReplacement,
274               (first, second) -> first,
275               LinkedHashMap::new));
276     }
277 
278     /**
279      * To group string replacements.
280      *
281      * @return the map
282      */
283     public Map<String, String> toGroupStringReplacements() {
284       return Stream.ofNullable(getGroupStringReplacements())
285           .flatMap(Collection::stream)
286           .collect(Collectors.toMap(
287               StringReplacement::getRegex,
288               StringReplacement::getReplacement,
289               (first, second) -> first,
290               LinkedHashMap::new));
291     }
292 
293   }
294 
295   /**
296    * The ldaptive properties.
297    */
298   @Data
299   public static class LdaptiveProperties {
300 
301     /**
302      * The ldap template with default configuration properties.
303      */
304     private Template template = Template.ACTIVE_DIRECTORY;
305 
306     /**
307      * The user base dn (like 'ou=people,dc=example,dc=org'). This value is always required.
308      */
309     private String userBaseDn;
310 
311     /**
312      * A list with refused usernames.
313      */
314     private List<String> refusedUsernames;
315 
316     /**
317      * The object class of the user (like 'inetOrgPerson'). The selected template contains a
318      * default.
319      */
320     private String userObjectClass;
321 
322     /**
323      * The username attribute of the user (like 'uid' or 'sAMAccountName'). The selected template
324      * contains a default.
325      */
326     private String usernameAttribute;
327 
328     /**
329      * The password attribute of the user (like 'userPassword'). If it is empty, a simple user bind
330      * will be done with the credentials of the user for authentication. If it is present, the
331      * connection to the ldap server must be done by a 'global' user and a password encoder that
332      * fits your requirements must be present. The default password encoder only supports SHA, that
333      * is insecure.
334      */
335     private String passwordAttribute;
336 
337     /**
338      * The password last set attribute (like 'pwdLastSet') can be used to activate the remember-me
339      * functionality.
340      */
341     private String passwordLastSetAttribute;
342 
343     /**
344      * The filter to find the user. If it is empty, it will be generated from 'userObjectClass' and
345      * 'usernameAttribute' like this '(&(objectClass=inetOrgPerson)(uid={0}))'.
346      */
347     private String userFindOneFilter;
348 
349     /**
350      * The scope to find a user. Default is 'one level'.
351      */
352     private SearchScope userFindOneSearchScope;
353 
354     /**
355      * The first name attribute of the user. Default is 'givenName'.
356      */
357     protected String firstNameAttribute;
358 
359     /**
360      * The last name attribute of the user. Default is 'sn'.
361      */
362     protected String lastNameAttribute;
363 
364     /**
365      * The email attribute of the user. Default is 'mail';
366      */
367     private String emailAttribute;
368 
369     /**
370      * The account control evaluator.
371      */
372     private AccountControlEvaluatorProperty accountControlEvaluator;
373 
374     /**
375      * The group fetch strategy.
376      */
377     private GroupFetchStrategy groupFetchStrategy;
378 
379     /**
380      * The member attribute.
381      */
382     private String memberAttribute;
383 
384     /**
385      * The group base dn (like 'ou=groups,dc=example,dc=org'). It's only required, if
386      * {@code groupFetchStrategy} is set to {@code GROUP_CONTAINS_USERS}.
387      */
388     private String groupBaseDn;
389 
390     /**
391      * The group search scope. It's only required, if {@code groupFetchStrategy} is set to
392      * {@code GROUP_CONTAINS_USERS},
393      */
394     private SearchScope groupSearchScope;
395 
396     /**
397      * The group object class. It's only required, if {@code groupFetchStrategy} is set to
398      * {@code GROUP_CONTAINS_USERS}
399      */
400     private String groupObjectClass;
401 
402     /**
403      * The group id attribute. It's only required, if {@code groupFetchStrategy} is set to
404      * {@code GROUP_CONTAINS_USERS}
405      */
406     private String groupIdAttribute;
407 
408     /**
409      * The group member attribute. It's only required, if {@code groupFetchStrategy} is set to
410      * {@code GROUP_CONTAINS_USERS}
411      */
412     private String groupMemberAttribute;
413 
414     /**
415      * The group member format. It's only required, if {@code groupFetchStrategy} is set to
416      * {@code GROUP_CONTAINS_USERS}
417      */
418     private String groupMemberFormat;
419 
420     /**
421      * The role mappings.
422      */
423     private List<SimpleMapping> roleMapping;
424 
425     /**
426      * The default roles.
427      */
428     private List<String> defaultRoles;
429 
430     /**
431      * The role prefix (like 'ROLE_').
432      */
433     private String rolePrefix;
434 
435     /**
436      * The role case transformation.
437      */
438     private CaseTransformation roleCaseTransformation;
439 
440     /**
441      * The string replacements for roles.
442      */
443     private List<StringReplacement> roleStringReplacements;
444 
445     /**
446      * Instantiates new ldaptive properties.
447      */
448     public LdaptiveProperties() {
449       super();
450     }
451 
452     /**
453      * The search scope.
454      */
455     public enum SearchScope {
456 
457       /**
458        * Base object search.
459        */
460       OBJECT,
461 
462       /**
463        * Single level search.
464        */
465       ONELEVEL,
466 
467       /**
468        * Whole subtree search.
469        */
470       SUBTREE,
471 
472       /**
473        * Subordinate subtree search. See draft-sermersheim-ldap-subordinate-scope.
474        */
475       SUBORDINATE
476     }
477 
478     /**
479      * The account control evaluator property.
480      */
481     public enum AccountControlEvaluatorProperty {
482 
483       /**
484        * The None.
485        */
486       NONE,
487 
488       /**
489        * The Active directory.
490        */
491       ACTIVE_DIRECTORY
492     }
493 
494     /**
495      * The group fetch strategy.
496      */
497     public enum GroupFetchStrategy {
498 
499       /**
500        * Groups will not be fetched.
501        */
502       NONE,
503 
504       /**
505        * User contains groups group-fetch strategy.
506        */
507       USER_CONTAINS_GROUPS,
508 
509       /**
510        * Group contains users group-fetch strategy.
511        */
512       GROUP_CONTAINS_USERS
513     }
514 
515     /**
516      * The templates for ldap authentication.
517      *
518      * @author Christian Bremer
519      */
520     public enum Template {
521 
522       /**
523        * Active directory template.
524        */
525       ACTIVE_DIRECTORY,
526 
527       /**
528        * Open ldap template.
529        */
530       OPEN_LDAP,
531 
532       /**
533        * User contains groups template.
534        */
535       USER_CONTAINS_GROUPS,
536 
537       /**
538        * Group contains users template.
539        */
540       GROUP_CONTAINS_USERS
541     }
542   }
543 
544   /**
545    * The simple mapping.
546    */
547   @Data
548   public static class SimpleMapping {
549 
550     private String source;
551 
552     private String target;
553 
554     /**
555      * Instantiates a new simple mapping.
556      */
557     public SimpleMapping() {
558       super();
559     }
560   }
561 
562   /**
563    * The case transformation.
564    */
565   public enum CaseTransformation {
566 
567     /**
568      * None case transformation.
569      */
570     NONE,
571 
572     /**
573      * To upper case transformation.
574      */
575     TO_UPPER_CASE,
576 
577     /**
578      * To lower case transformation.
579      */
580     TO_LOWER_CASE
581   }
582 
583   /**
584    * The string replacement.
585    */
586   @Data
587   public static class StringReplacement {
588 
589     /**
590      * The regular expression to which the string is to be matched. '{@code [- ]}' for example would
591      * replace every '-' and every space.
592      */
593     private String regex;
594 
595     /**
596      * The string to be substituted for each match.
597      */
598     private String replacement;
599 
600     /**
601      * Instantiates a new string replacement.
602      */
603     public StringReplacement() {
604       super();
605     }
606   }
607 
608   /**
609    * The actuator properties.
610    */
611   @Data
612   public static class ActuatorProperties {
613 
614     private List<String> readRoles = new ArrayList<>();
615 
616     private List<String> writeRoles = new ArrayList<>();
617 
618     /**
619      * Instantiates actuator properties.
620      */
621     public ActuatorProperties() {
622       super();
623     }
624 
625     /**
626      * Gets read roles.
627      *
628      * @return the read roles
629      */
630     public List<String> getReadRoles() {
631       if (isEmpty(readRoles)) {
632         return List.of("ROLE_ADMIN", "ROLE_ACTUATOR", "ROLE_ACTUATOR_ADMIN");
633       }
634       return readRoles;
635     }
636 
637     /**
638      * Gets write roles.
639      *
640      * @return the write roles
641      */
642     public List<String> getWriteRoles() {
643       if (isEmpty(writeRoles)) {
644         return List.of("ROLE_ADMIN", "ROLE_ACTUATOR_ADMIN");
645       }
646       return writeRoles;
647     }
648   }
649 
650 }