0
# Principal Resolution
1
2
Principal resolution is the process of identifying and constructing user principals with their associated attributes after successful authentication. The CAS authentication API provides a flexible framework for resolving principals from various sources and managing their attributes through configurable merging strategies.
3
4
## Core Principal Interface
5
6
```java { .api }
7
package org.apereo.cas.authentication.principal;
8
9
import java.io.Serializable;
10
import java.util.List;
11
import java.util.Map;
12
13
public interface Principal extends Serializable {
14
String getId();
15
Map<String, List<Object>> getAttributes();
16
}
17
```
18
19
## Principal Implementations
20
21
### Simple Principal
22
23
The default implementation that provides immutable attribute storage:
24
25
```java { .api }
26
package org.apereo.cas.authentication.principal;
27
28
import com.fasterxml.jackson.annotation.JsonCreator;
29
import com.fasterxml.jackson.annotation.JsonProperty;
30
import java.util.List;
31
import java.util.Map;
32
import java.util.TreeMap;
33
34
public class SimplePrincipal implements Principal {
35
private final String id;
36
private final Map<String, List<Object>> attributes;
37
38
@JsonCreator
39
public SimplePrincipal(@JsonProperty("id") String id,
40
@JsonProperty("attributes") Map<String, List<Object>> attributes) {
41
this.id = id;
42
this.attributes = attributes != null ?
43
new TreeMap<>(String.CASE_INSENSITIVE_ORDER) {{
44
putAll(attributes);
45
}} : new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
46
}
47
48
public SimplePrincipal(String id) {
49
this(id, null);
50
}
51
52
public String getId() { return id; }
53
54
public Map<String, List<Object>> getAttributes() { return attributes; }
55
56
@Override
57
public boolean equals(Object obj) {
58
if (obj == null || !this.getClass().equals(obj.getClass())) {
59
return false;
60
}
61
return getId().equals(((SimplePrincipal) obj).getId());
62
}
63
64
@Override
65
public int hashCode() {
66
return getId().hashCode();
67
}
68
}
69
```
70
71
### Null Principal
72
73
Null object pattern implementation for cases where no principal is available:
74
75
```java { .api }
76
package org.apereo.cas.authentication.principal;
77
78
import java.util.Collections;
79
import java.util.List;
80
import java.util.Map;
81
82
public final class NullPrincipal implements Principal {
83
private static final NullPrincipal INSTANCE = new NullPrincipal();
84
85
private NullPrincipal() {}
86
87
public static NullPrincipal getInstance() {
88
return INSTANCE;
89
}
90
91
public String getId() {
92
return null;
93
}
94
95
public Map<String, List<Object>> getAttributes() {
96
return Collections.emptyMap();
97
}
98
99
@Override
100
public String toString() {
101
return "[Principal id=null, attributes={}]";
102
}
103
}
104
```
105
106
## Principal Factory
107
108
Principal factories are responsible for creating principal instances:
109
110
```java { .api }
111
package org.apereo.cas.authentication.principal;
112
113
import java.util.List;
114
import java.util.Map;
115
116
public interface PrincipalFactory {
117
Principal createPrincipal(String id);
118
Principal createPrincipal(String id, Map<String, List<Object>> attributes);
119
Principal createPrincipal(String id, Map<String, List<Object>> attributes,
120
Map<String, List<Object>> originalAttributes);
121
}
122
```
123
124
### Default Principal Factory
125
126
```java { .api }
127
package org.apereo.cas.authentication.principal;
128
129
import java.util.List;
130
import java.util.Map;
131
132
public class DefaultPrincipalFactory implements PrincipalFactory {
133
134
public Principal createPrincipal(String id) {
135
return new SimplePrincipal(id);
136
}
137
138
public Principal createPrincipal(String id, Map<String, List<Object>> attributes) {
139
return new SimplePrincipal(id, attributes);
140
}
141
142
public Principal createPrincipal(String id,
143
Map<String, List<Object>> attributes,
144
Map<String, List<Object>> originalAttributes) {
145
return new SimplePrincipal(id, attributes);
146
}
147
}
148
```
149
150
### Groovy Principal Factory
151
152
Factory that uses Groovy scripts for dynamic principal creation:
153
154
```java { .api }
155
package org.apereo.cas.authentication.principal;
156
157
import org.apereo.cas.util.scripting.ExecutableCompiledGroovyScript;
158
import java.util.List;
159
import java.util.Map;
160
161
public class GroovyPrincipalFactory implements PrincipalFactory {
162
private final ExecutableCompiledGroovyScript watchableScript;
163
164
public GroovyPrincipalFactory(ExecutableCompiledGroovyScript watchableScript) {
165
this.watchableScript = watchableScript;
166
}
167
168
public Principal createPrincipal(String id, Map<String, List<Object>> attributes) {
169
Object result = watchableScript.execute(id, attributes, Principal.class);
170
if (result instanceof Principal) {
171
return (Principal) result;
172
}
173
return new SimplePrincipal(id, attributes);
174
}
175
176
public Principal createPrincipal(String id) {
177
return createPrincipal(id, null);
178
}
179
180
public Principal createPrincipal(String id,
181
Map<String, List<Object>> attributes,
182
Map<String, List<Object>> originalAttributes) {
183
Object result = watchableScript.execute(id, attributes, originalAttributes, Principal.class);
184
if (result instanceof Principal) {
185
return (Principal) result;
186
}
187
return new SimplePrincipal(id, attributes);
188
}
189
}
190
```
191
192
### RESTful Principal Factory
193
194
Factory that creates principals by calling REST endpoints:
195
196
```java { .api }
197
package org.apereo.cas.authentication.principal;
198
199
import org.springframework.http.HttpEntity;
200
import org.springframework.http.HttpMethod;
201
import org.springframework.web.client.RestTemplate;
202
import java.util.List;
203
import java.util.Map;
204
205
public class RestfulPrincipalFactory implements PrincipalFactory {
206
private final RestTemplate restTemplate;
207
private final String endpoint;
208
209
public RestfulPrincipalFactory(RestTemplate restTemplate, String endpoint) {
210
this.restTemplate = restTemplate;
211
this.endpoint = endpoint;
212
}
213
214
public Principal createPrincipal(String id, Map<String, List<Object>> attributes) {
215
try {
216
Map<String, Object> request = Map.of("id", id, "attributes", attributes);
217
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request);
218
219
Map<String, Object> response = restTemplate.exchange(
220
endpoint, HttpMethod.POST, entity, Map.class).getBody();
221
222
if (response != null && response.containsKey("id")) {
223
String principalId = (String) response.get("id");
224
Map<String, List<Object>> principalAttributes =
225
(Map<String, List<Object>>) response.get("attributes");
226
return new SimplePrincipal(principalId, principalAttributes);
227
}
228
} catch (Exception e) {
229
// Log error and fall back to default behavior
230
}
231
232
return new SimplePrincipal(id, attributes);
233
}
234
235
public Principal createPrincipal(String id) {
236
return createPrincipal(id, null);
237
}
238
239
public Principal createPrincipal(String id,
240
Map<String, List<Object>> attributes,
241
Map<String, List<Object>> originalAttributes) {
242
return createPrincipal(id, attributes);
243
}
244
}
245
```
246
247
## Principal Factory Utilities
248
249
```java { .api }
250
package org.apereo.cas.authentication.principal;
251
252
public final class PrincipalFactoryUtils {
253
private PrincipalFactoryUtils() {}
254
255
public static PrincipalFactory newPrincipalFactory() {
256
return new DefaultPrincipalFactory();
257
}
258
}
259
```
260
261
## Principal Election Strategy
262
263
When multiple authentication handlers produce different principals, an election strategy determines which principal to use:
264
265
```java { .api }
266
package org.apereo.cas.authentication.principal;
267
268
import org.apereo.cas.authentication.Authentication;
269
import java.util.Collection;
270
271
public interface PrincipalElectionStrategy {
272
Principal electPrincipal(Collection<Authentication> authentications);
273
}
274
```
275
276
### Default Election Strategy
277
278
```java { .api }
279
package org.apereo.cas.authentication.principal;
280
281
import org.apereo.cas.authentication.Authentication;
282
import java.util.Collection;
283
284
public class DefaultPrincipalElectionStrategy implements PrincipalElectionStrategy {
285
286
public Principal electPrincipal(Collection<Authentication> authentications) {
287
return authentications.stream()
288
.findFirst()
289
.map(Authentication::getPrincipal)
290
.orElse(NullPrincipal.getInstance());
291
}
292
}
293
```
294
295
### Chaining Election Strategy
296
297
```java { .api }
298
package org.apereo.cas.authentication.principal;
299
300
import org.apereo.cas.authentication.Authentication;
301
import java.util.ArrayList;
302
import java.util.Collection;
303
import java.util.List;
304
305
public class ChainingPrincipalElectionStrategy implements PrincipalElectionStrategy {
306
private final List<PrincipalElectionStrategy> chain = new ArrayList<>();
307
308
public void addStrategy(PrincipalElectionStrategy strategy) {
309
chain.add(strategy);
310
}
311
312
public Principal electPrincipal(Collection<Authentication> authentications) {
313
return chain.stream()
314
.map(strategy -> strategy.electPrincipal(authentications))
315
.filter(principal -> principal != null && !NullPrincipal.getInstance().equals(principal))
316
.findFirst()
317
.orElse(NullPrincipal.getInstance());
318
}
319
}
320
```
321
322
## Principal Resolvers
323
324
Principal resolvers are responsible for enriching principals with attributes from external sources:
325
326
```java { .api }
327
package org.apereo.cas.authentication.principal;
328
329
import org.apereo.cas.authentication.AuthenticationHandler;
330
import org.apereo.cas.authentication.Credential;
331
import java.util.Optional;
332
333
public interface PrincipalResolver {
334
Optional<Principal> resolve(Credential credential,
335
Optional<Principal> currentPrincipal,
336
Optional<AuthenticationHandler> handler);
337
boolean supports(Credential credential);
338
IPersonAttributeDao getAttributeRepository();
339
}
340
```
341
342
### Echoing Principal Resolver
343
344
Simple resolver that returns the principal as-is:
345
346
```java { .api }
347
package org.apereo.cas.authentication.principal.resolvers;
348
349
import org.apereo.cas.authentication.AuthenticationHandler;
350
import org.apereo.cas.authentication.Credential;
351
import org.apereo.cas.authentication.principal.Principal;
352
import org.apereo.cas.authentication.principal.PrincipalResolver;
353
import java.util.Optional;
354
355
public class EchoingPrincipalResolver implements PrincipalResolver {
356
357
public Optional<Principal> resolve(Credential credential,
358
Optional<Principal> currentPrincipal,
359
Optional<AuthenticationHandler> handler) {
360
return currentPrincipal;
361
}
362
363
public boolean supports(Credential credential) {
364
return true;
365
}
366
367
public IPersonAttributeDao getAttributeRepository() {
368
return null;
369
}
370
}
371
```
372
373
### Proxying Principal Resolver
374
375
Resolver that creates proxy principals for service authentication:
376
377
```java { .api }
378
package org.apereo.cas.authentication.principal.resolvers;
379
380
import org.apereo.cas.authentication.AuthenticationHandler;
381
import org.apereo.cas.authentication.Credential;
382
import org.apereo.cas.authentication.principal.Principal;
383
import org.apereo.cas.authentication.principal.PrincipalFactory;
384
import org.apereo.cas.authentication.principal.PrincipalResolver;
385
import java.util.Optional;
386
387
public class ProxyingPrincipalResolver implements PrincipalResolver {
388
private final PrincipalFactory principalFactory;
389
390
public ProxyingPrincipalResolver(PrincipalFactory principalFactory) {
391
this.principalFactory = principalFactory;
392
}
393
394
public Optional<Principal> resolve(Credential credential,
395
Optional<Principal> currentPrincipal,
396
Optional<AuthenticationHandler> handler) {
397
String principalId = credential.getId();
398
if (principalId != null) {
399
Principal principal = principalFactory.createPrincipal(principalId);
400
return Optional.of(principal);
401
}
402
return currentPrincipal;
403
}
404
405
public boolean supports(Credential credential) {
406
return credential != null && credential.getId() != null;
407
}
408
409
public IPersonAttributeDao getAttributeRepository() {
410
return null;
411
}
412
}
413
```
414
415
### Chaining Principal Resolver
416
417
Resolver that chains multiple resolvers together:
418
419
```java { .api }
420
package org.apereo.cas.authentication.principal.resolvers;
421
422
import org.apereo.cas.authentication.AuthenticationHandler;
423
import org.apereo.cas.authentication.Credential;
424
import org.apereo.cas.authentication.principal.Principal;
425
import org.apereo.cas.authentication.principal.PrincipalResolver;
426
import java.util.ArrayList;
427
import java.util.List;
428
import java.util.Optional;
429
430
public class ChainingPrincipalResolver implements PrincipalResolver {
431
private final List<PrincipalResolver> chain = new ArrayList<>();
432
433
public void addResolver(PrincipalResolver resolver) {
434
chain.add(resolver);
435
}
436
437
public Optional<Principal> resolve(Credential credential,
438
Optional<Principal> currentPrincipal,
439
Optional<AuthenticationHandler> handler) {
440
Optional<Principal> result = currentPrincipal;
441
442
for (PrincipalResolver resolver : chain) {
443
if (resolver.supports(credential)) {
444
result = resolver.resolve(credential, result, handler);
445
if (result.isPresent()) {
446
break;
447
}
448
}
449
}
450
451
return result;
452
}
453
454
public boolean supports(Credential credential) {
455
return chain.stream().anyMatch(resolver -> resolver.supports(credential));
456
}
457
458
public IPersonAttributeDao getAttributeRepository() {
459
return chain.stream()
460
.map(PrincipalResolver::getAttributeRepository)
461
.filter(Objects::nonNull)
462
.findFirst()
463
.orElse(null);
464
}
465
}
466
```
467
468
## Attribute Merging Strategies
469
470
Attribute mergers determine how to combine attributes from multiple sources:
471
472
```java { .api }
473
package org.apereo.cas.authentication.principal.merger;
474
475
import java.util.List;
476
import java.util.Map;
477
478
public interface AttributeMerger {
479
Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
480
Map<String, List<Object>> toMerge);
481
}
482
```
483
484
### Base Additive Attribute Merger
485
486
```java { .api }
487
package org.apereo.cas.authentication.principal.merger;
488
489
import java.util.ArrayList;
490
import java.util.LinkedHashMap;
491
import java.util.List;
492
import java.util.Map;
493
494
public abstract class BaseAdditiveAttributeMerger implements AttributeMerger {
495
496
public final Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
497
Map<String, List<Object>> toMerge) {
498
Map<String, List<Object>> results = new LinkedHashMap<>(toModify);
499
500
for (Map.Entry<String, List<Object>> entry : toMerge.entrySet()) {
501
String key = entry.getKey();
502
List<Object> values = entry.getValue();
503
504
if (results.containsKey(key)) {
505
List<Object> existingValues = new ArrayList<>(results.get(key));
506
List<Object> mergedValues = mergeValues(existingValues, values);
507
results.put(key, mergedValues);
508
} else {
509
results.put(key, new ArrayList<>(values));
510
}
511
}
512
513
return results;
514
}
515
516
protected abstract List<Object> mergeValues(List<Object> existing, List<Object> additional);
517
}
518
```
519
520
### Multivalued Attribute Merger
521
522
Adds all values from both sources:
523
524
```java { .api }
525
package org.apereo.cas.authentication.principal.merger;
526
527
import java.util.ArrayList;
528
import java.util.List;
529
530
public class MultivaluedAttributeMerger extends BaseAdditiveAttributeMerger {
531
532
protected List<Object> mergeValues(List<Object> existing, List<Object> additional) {
533
List<Object> merged = new ArrayList<>(existing);
534
merged.addAll(additional);
535
return merged;
536
}
537
}
538
```
539
540
### Non-colliding Attribute Adder
541
542
Only adds attributes that don't already exist:
543
544
```java { .api }
545
package org.apereo.cas.authentication.principal.merger;
546
547
import java.util.List;
548
import java.util.Map;
549
import java.util.LinkedHashMap;
550
551
public class NoncollidingAttributeAdder implements AttributeMerger {
552
553
public Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
554
Map<String, List<Object>> toMerge) {
555
Map<String, List<Object>> results = new LinkedHashMap<>(toModify);
556
557
for (Map.Entry<String, List<Object>> entry : toMerge.entrySet()) {
558
String key = entry.getKey();
559
if (!results.containsKey(key)) {
560
results.put(key, entry.getValue());
561
}
562
}
563
564
return results;
565
}
566
}
567
```
568
569
### Replacing Attribute Adder
570
571
Replaces existing attributes with new values:
572
573
```java { .api }
574
package org.apereo.cas.authentication.principal.merger;
575
576
import java.util.LinkedHashMap;
577
import java.util.List;
578
import java.util.Map;
579
580
public class ReplacingAttributeAdder implements AttributeMerger {
581
582
public Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
583
Map<String, List<Object>> toMerge) {
584
Map<String, List<Object>> results = new LinkedHashMap<>(toModify);
585
results.putAll(toMerge);
586
return results;
587
}
588
}
589
```
590
591
### Return Changes Attribute Merger
592
593
Only returns the new/changed attributes:
594
595
```java { .api }
596
package org.apereo.cas.authentication.principal.merger;
597
598
import java.util.LinkedHashMap;
599
import java.util.List;
600
import java.util.Map;
601
602
public class ReturnChangesAttributeMerger implements AttributeMerger {
603
604
public Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
605
Map<String, List<Object>> toMerge) {
606
return new LinkedHashMap<>(toMerge);
607
}
608
}
609
```
610
611
### Return Original Attribute Merger
612
613
Always returns the original attributes unchanged:
614
615
```java { .api }
616
package org.apereo.cas.authentication.principal.merger;
617
618
import java.util.LinkedHashMap;
619
import java.util.List;
620
import java.util.Map;
621
622
public class ReturnOriginalAttributeMerger implements AttributeMerger {
623
624
public Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
625
Map<String, List<Object>> toMerge) {
626
return new LinkedHashMap<>(toModify);
627
}
628
}
629
```
630
631
## Principal Name Transformation
632
633
```java { .api }
634
package org.apereo.cas.authentication.principal;
635
636
import org.apereo.cas.util.transforms.PrefixSuffixPrincipalNameTransformer;
637
import org.apereo.cas.util.transforms.RegexPrincipalNameTransformer;
638
import org.apereo.cas.util.transforms.UpperCasePrincipalNameTransformer;
639
import org.apereo.cas.util.transforms.LowerCasePrincipalNameTransformer;
640
641
public final class PrincipalNameTransformerUtils {
642
643
public static PrincipalNameTransformer newPrefixSuffixTransformer(String prefix, String suffix) {
644
return new PrefixSuffixPrincipalNameTransformer(prefix, suffix);
645
}
646
647
public static PrincipalNameTransformer newRegexTransformer(String pattern, String replacement) {
648
return new RegexPrincipalNameTransformer(pattern, replacement);
649
}
650
651
public static PrincipalNameTransformer newUpperCaseTransformer() {
652
return new UpperCasePrincipalNameTransformer();
653
}
654
655
public static PrincipalNameTransformer newLowerCaseTransformer() {
656
return new LowerCasePrincipalNameTransformer();
657
}
658
}
659
```
660
661
## Resolution Execution Plan
662
663
```java { .api }
664
package org.apereo.cas.authentication.principal;
665
666
import org.apereo.cas.authentication.AuthenticationHandler;
667
import java.util.Collection;
668
669
public interface PrincipalResolutionExecutionPlan {
670
void registerPrincipalResolver(PrincipalResolver resolver);
671
void registerPrincipalResolver(PrincipalResolver resolver, AuthenticationHandler handler);
672
Collection<PrincipalResolver> getRegisteredPrincipalResolvers();
673
}
674
675
public class DefaultPrincipalResolutionExecutionPlan implements PrincipalResolutionExecutionPlan {
676
private final Map<AuthenticationHandler, PrincipalResolver> resolverMap = new LinkedHashMap<>();
677
private PrincipalResolver globalResolver;
678
679
public void registerPrincipalResolver(PrincipalResolver resolver) {
680
this.globalResolver = resolver;
681
}
682
683
public void registerPrincipalResolver(PrincipalResolver resolver, AuthenticationHandler handler) {
684
resolverMap.put(handler, resolver);
685
}
686
687
public Collection<PrincipalResolver> getRegisteredPrincipalResolvers() {
688
Collection<PrincipalResolver> resolvers = new ArrayList<>(resolverMap.values());
689
if (globalResolver != null) {
690
resolvers.add(globalResolver);
691
}
692
return resolvers;
693
}
694
}
695
```
696
697
## Configuration Examples
698
699
### Spring Configuration
700
701
```java { .api }
702
@Configuration
703
public class PrincipalResolutionConfiguration {
704
705
@Bean
706
public PrincipalFactory principalFactory() {
707
return new DefaultPrincipalFactory();
708
}
709
710
@Bean
711
public PrincipalElectionStrategy principalElectionStrategy() {
712
return new DefaultPrincipalElectionStrategy();
713
}
714
715
@Bean
716
public AttributeMerger attributeMerger() {
717
return new MultivaluedAttributeMerger();
718
}
719
720
@Bean
721
public PrincipalResolver echoingPrincipalResolver() {
722
return new EchoingPrincipalResolver();
723
}
724
}
725
```
726
727
### Programmatic Usage
728
729
```java { .api }
730
// Create principal with attributes
731
Map<String, List<Object>> attributes = Map.of(
732
"email", List.of("user@example.com"),
733
"roles", List.of("admin", "user"),
734
"department", List.of("IT")
735
);
736
737
PrincipalFactory factory = new DefaultPrincipalFactory();
738
Principal principal = factory.createPrincipal("username", attributes);
739
740
// Merge attributes from multiple sources
741
AttributeMerger merger = new MultivaluedAttributeMerger();
742
Map<String, List<Object>> additionalAttrs = Map.of(
743
"groups", List.of("developers", "admins"),
744
"roles", List.of("manager") // This will be merged with existing roles
745
);
746
747
Map<String, List<Object>> mergedAttrs = merger.mergeAttributes(
748
principal.getAttributes(), additionalAttrs);
749
750
// Create election strategy chain
751
ChainingPrincipalElectionStrategy electionStrategy = new ChainingPrincipalElectionStrategy();
752
electionStrategy.addStrategy(new DefaultPrincipalElectionStrategy());
753
```
754
755
Principal resolution provides the foundation for user identity management in CAS, allowing flexible attribute resolution, merging strategies, and principal election to support complex enterprise requirements.