Skip to content

Commit ce36fc1

Browse files
committed
Add FactorGrantedAuthority
Closes gh-17996
1 parent 477a456 commit ce36fc1

File tree

10 files changed

+371
-4
lines changed

10 files changed

+371
-4
lines changed

config/src/test/java/org/springframework/security/SerializationSamples.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
import org.springframework.security.core.Authentication;
9595
import org.springframework.security.core.GrantedAuthority;
9696
import org.springframework.security.core.authority.AuthorityUtils;
97+
import org.springframework.security.core.authority.FactorGrantedAuthority;
9798
import org.springframework.security.core.context.SecurityContext;
9899
import org.springframework.security.core.context.SecurityContextImpl;
99100
import org.springframework.security.core.context.TransientSecurityContext;
@@ -584,6 +585,8 @@ final class SerializationSamples {
584585
token.setDetails(details);
585586
return token;
586587
});
588+
generatorByClassName.put(FactorGrantedAuthority.class,
589+
(r) -> FactorGrantedAuthority.withAuthority("profile:read").issuedAt(Instant.now()).build());
587590
generatorByClassName.put(UsernamePasswordAuthenticationToken.class, (r) -> {
588591
var token = UsernamePasswordAuthenticationToken.unauthenticated(user, "creds");
589592
token.setDetails(details);
214 Bytes
Binary file not shown.

core/spring-security-core.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies {
2727
optional 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor'
2828

2929
testImplementation 'commons-collections:commons-collections'
30+
testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
3031
testImplementation 'io.projectreactor:reactor-test'
3132
testImplementation "org.assertj:assertj-core"
3233
testImplementation "org.junit.jupiter:junit-jupiter-api"

core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
import org.springframework.security.core.AuthenticationException;
4747
import org.springframework.security.core.GrantedAuthorities;
4848
import org.springframework.security.core.GrantedAuthority;
49-
import org.springframework.security.core.authority.SimpleGrantedAuthority;
49+
import org.springframework.security.core.authority.FactorGrantedAuthority;
5050
import org.springframework.security.core.context.SecurityContext;
5151
import org.springframework.security.core.session.SessionDestroyedEvent;
5252
import org.springframework.util.Assert;
@@ -214,7 +214,7 @@ private Set<GrantedAuthority> getAuthorities(Set<Principal> principals) {
214214
}
215215
}
216216
}
217-
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
217+
authorities.add(FactorGrantedAuthority.fromAuthority(AUTHORITY));
218218
return authorities;
219219
}
220220

core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import org.springframework.security.core.AuthenticationException;
2626
import org.springframework.security.core.GrantedAuthorities;
2727
import org.springframework.security.core.GrantedAuthority;
28-
import org.springframework.security.core.authority.SimpleGrantedAuthority;
28+
import org.springframework.security.core.authority.FactorGrantedAuthority;
2929
import org.springframework.security.core.userdetails.UserDetails;
3030
import org.springframework.security.core.userdetails.UserDetailsService;
3131
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -65,7 +65,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
6565
try {
6666
UserDetails user = this.userDetailsService.loadUserByUsername(consumed.getUsername());
6767
Collection<GrantedAuthority> authorities = new HashSet<>(user.getAuthorities());
68-
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
68+
authorities.add(FactorGrantedAuthority.fromAuthority(AUTHORITY));
6969
OneTimeTokenAuthentication authenticated = new OneTimeTokenAuthentication(user, authorities);
7070
authenticated.setDetails(otpAuthenticationToken.getDetails());
7171
return authenticated;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright 2004-present 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+
* 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,
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.springframework.security.core.authority;
18+
19+
import java.time.Instant;
20+
import java.util.Objects;
21+
22+
import org.jspecify.annotations.Nullable;
23+
24+
import org.springframework.security.core.GrantedAuthority;
25+
import org.springframework.util.Assert;
26+
27+
/**
28+
* A {@link GrantedAuthority} specifically used for indicating the factor used at time of
29+
* authentication.
30+
*
31+
* @author Yoobin Yoon
32+
* @author Rob Winch
33+
* @since 7.0
34+
*/
35+
public final class FactorGrantedAuthority implements GrantedAuthority {
36+
37+
private static final long serialVersionUID = 1998010439847123984L;
38+
39+
private final String authority;
40+
41+
private final Instant issuedAt;
42+
43+
@SuppressWarnings("NullAway")
44+
private FactorGrantedAuthority(String authority, Instant issuedAt) {
45+
Assert.notNull(authority, "authority cannot be null");
46+
Assert.notNull(issuedAt, "issuedAt cannot be null");
47+
this.authority = authority;
48+
this.issuedAt = issuedAt;
49+
}
50+
51+
/**
52+
* Creates a new {@link Builder} with the specified authority.
53+
* @param authority the authority value (must not be null or empty)
54+
* @return a new {@link Builder}
55+
*/
56+
public static Builder withAuthority(String authority) {
57+
return new Builder(authority);
58+
}
59+
60+
/**
61+
* Creates a new {@link Builder} with the specified factor which is automatically
62+
* prefixed with "FACTOR_".
63+
* @param factor the factor value which is automatically prefixed with "FACTOR_" (must
64+
* not be null or empty)
65+
* @return a new {@link Builder}
66+
*/
67+
public static Builder withFactor(String factor) {
68+
Assert.hasText(factor, "factor cannot be empty");
69+
Assert.isTrue(!factor.startsWith("FACTOR_"), () -> "factor cannot start with 'FACTOR_' got '" + factor + "'");
70+
return withAuthority("FACTOR_" + factor);
71+
}
72+
73+
/**
74+
* Shortcut for {@code withAuthority(authority).build()}.
75+
* @param authority the authority value (must not be null or empty)
76+
* @return a new {@link FactorGrantedAuthority}
77+
*/
78+
public static FactorGrantedAuthority fromAuthority(String authority) {
79+
return withAuthority(authority).build();
80+
}
81+
82+
/**
83+
* Shortcut for {@code withFactor(factor).build()}.
84+
* @param factor the factor value which is automatically prefixed with "FACTOR_" (must
85+
* not be null or empty)
86+
* @return a new {@link FactorGrantedAuthority}
87+
*/
88+
public static FactorGrantedAuthority fromFactor(String factor) {
89+
return withFactor(factor).build();
90+
}
91+
92+
@Override
93+
public String getAuthority() {
94+
return this.authority;
95+
}
96+
97+
/**
98+
* Returns the instant when this authority was issued.
99+
* @return the issued-at instant
100+
*/
101+
public Instant getIssuedAt() {
102+
return this.issuedAt;
103+
}
104+
105+
@Override
106+
public boolean equals(Object obj) {
107+
if (this == obj) {
108+
return true;
109+
}
110+
if (obj instanceof FactorGrantedAuthority fga) {
111+
return this.authority.equals(fga.authority) && this.issuedAt.equals(fga.issuedAt);
112+
}
113+
return false;
114+
}
115+
116+
@Override
117+
public int hashCode() {
118+
return Objects.hash(this.authority, this.issuedAt);
119+
}
120+
121+
@Override
122+
public String toString() {
123+
StringBuilder sb = new StringBuilder();
124+
sb.append("FactorGrantedAuthority [");
125+
sb.append("authority=").append(this.authority);
126+
sb.append(", issuedAt=").append(this.issuedAt);
127+
sb.append("]");
128+
return sb.toString();
129+
}
130+
131+
/**
132+
* Builder for {@link FactorGrantedAuthority}.
133+
*/
134+
public static final class Builder {
135+
136+
private final String authority;
137+
138+
private @Nullable Instant issuedAt;
139+
140+
private Builder(String authority) {
141+
Assert.hasText(authority, "A granted authority textual representation is required");
142+
this.authority = authority;
143+
}
144+
145+
/**
146+
* Sets the instant when this authority was issued.
147+
* @param issuedAt the issued-at instant
148+
* @return this builder
149+
*/
150+
public Builder issuedAt(Instant issuedAt) {
151+
Assert.notNull(issuedAt, "issuedAt cannot be null");
152+
this.issuedAt = issuedAt;
153+
return this;
154+
}
155+
156+
/**
157+
* Builds a new {@link FactorGrantedAuthority}.
158+
* <p>
159+
* If {@code issuedAt} is not set, it defaults to {@link Instant#now()}.
160+
* @return a new {@link FactorGrantedAuthority}
161+
* @throws IllegalArgumentException if temporal constraints are invalid
162+
*/
163+
public FactorGrantedAuthority build() {
164+
if (this.issuedAt == null) {
165+
this.issuedAt = Instant.now();
166+
}
167+
168+
return new FactorGrantedAuthority(this.authority, this.issuedAt);
169+
}
170+
171+
}
172+
173+
}

core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.security.authentication.BadCredentialsException;
2626
import org.springframework.security.authentication.RememberMeAuthenticationToken;
2727
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
28+
import org.springframework.security.core.authority.FactorGrantedAuthority;
2829
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2930
import org.springframework.security.core.userdetails.User;
3031

@@ -60,6 +61,7 @@ public void setupModule(SetupContext context) {
6061
context.setMixInAnnotations(AnonymousAuthenticationToken.class, AnonymousAuthenticationTokenMixin.class);
6162
context.setMixInAnnotations(RememberMeAuthenticationToken.class, RememberMeAuthenticationTokenMixin.class);
6263
context.setMixInAnnotations(SimpleGrantedAuthority.class, SimpleGrantedAuthorityMixin.class);
64+
context.setMixInAnnotations(FactorGrantedAuthority.class, FactorGrantedAuthorityMixin.class);
6365
context.setMixInAnnotations(Collections.unmodifiableSet(Collections.emptySet()).getClass(),
6466
UnmodifiableSetMixin.class);
6567
context.setMixInAnnotations(Collections.unmodifiableList(Collections.emptyList()).getClass(),
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2004-present 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+
* 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,
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.springframework.security.jackson2;
18+
19+
import java.time.Instant;
20+
21+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
22+
import com.fasterxml.jackson.annotation.JsonCreator;
23+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
24+
import com.fasterxml.jackson.annotation.JsonProperty;
25+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
26+
27+
/**
28+
* Jackson Mixin class helps in serialize/deserialize
29+
* {@link org.springframework.security.core.authority.SimpleGrantedAuthority}.
30+
*
31+
* <pre>
32+
* ObjectMapper mapper = new ObjectMapper();
33+
* mapper.registerModule(new CoreJackson2Module());
34+
* </pre>
35+
*
36+
* @author Rob Winch
37+
* @since 7.0
38+
* @see CoreJackson2Module
39+
* @see SecurityJackson2Modules
40+
*/
41+
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
42+
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE,
43+
getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
44+
@JsonIgnoreProperties(ignoreUnknown = true)
45+
abstract class FactorGrantedAuthorityMixin {
46+
47+
/**
48+
* Mixin Constructor.
49+
* @param authority the authority
50+
*/
51+
@JsonCreator
52+
FactorGrantedAuthorityMixin(@JsonProperty("authority") String authority,
53+
@JsonProperty("issuedAt") Instant issuedAt) {
54+
}
55+
56+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2004-present 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+
* 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,
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.springframework.security.core.authority;
18+
19+
import java.time.Instant;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
25+
26+
/**
27+
* Tests {@link FactorGrantedAuthority}.
28+
*
29+
* @author Yoobin Yoon
30+
* @author Rob Winch
31+
*/
32+
public class FactorGrantedAuthorityTests {
33+
34+
@Test
35+
public void buildWhenOnlyAuthorityThenDefaultsIssuedAtToNow() {
36+
Instant before = Instant.now();
37+
38+
FactorGrantedAuthority authority = FactorGrantedAuthority.withAuthority("profile:read").build();
39+
40+
Instant after = Instant.now();
41+
42+
assertThat(authority.getAuthority()).isEqualTo("profile:read");
43+
assertThat(authority.getIssuedAt()).isBetween(before, after);
44+
}
45+
46+
@Test
47+
public void buildWhenAllFieldsSetThenCreatesCorrectly() {
48+
Instant issuedAt = Instant.now();
49+
50+
FactorGrantedAuthority authority = FactorGrantedAuthority.withAuthority("admin:write")
51+
.issuedAt(issuedAt)
52+
.build();
53+
54+
assertThat(authority.getAuthority()).isEqualTo("admin:write");
55+
assertThat(authority.getIssuedAt()).isEqualTo(issuedAt);
56+
}
57+
58+
@Test
59+
public void buildWhenNullAuthorityThenThrowsException() {
60+
assertThatIllegalArgumentException().isThrownBy(() -> FactorGrantedAuthority.withAuthority(null))
61+
.withMessage("A granted authority textual representation is required");
62+
}
63+
64+
@Test
65+
public void buildWhenEmptyAuthorityThenThrowsException() {
66+
assertThatIllegalArgumentException().isThrownBy(() -> FactorGrantedAuthority.withAuthority(""))
67+
.withMessage("A granted authority textual representation is required");
68+
}
69+
70+
}

0 commit comments

Comments
 (0)