Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f6f001b

Browse files
committed
Add TimestampedGrantedAuthority for time-based authorization
- Implement GrantedAuthority with temporal constraints (issuedAt, notBefore, expiresAt) - Use Builder pattern for flexible construction - Default issuedAt to Instant.now() when not specified - Add comprehensive tests Closes gh-17864 Signed-off-by: yybmion <yunyubin54@gmail.com>
1 parent 5da2121 commit f6f001b

File tree

2 files changed

+339
-0
lines changed

2 files changed

+339
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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+
* Time-based implementation of {@link GrantedAuthority}.
29+
*
30+
* <p>
31+
* Represents an authority granted to the
32+
* {@link org.springframework.security.core.Authentication Authentication} object with
33+
* temporal constraints. This implementation allows authorities to have:
34+
* <ul>
35+
* <li>An issued-at timestamp indicating when the authority was granted</li>
36+
* <li>An optional not-before timestamp indicating when the authority becomes valid</li>
37+
* <li>An optional expires-at timestamp indicating when the authority expires</li>
38+
* </ul>
39+
*
40+
* <p>
41+
* This is particularly useful for:
42+
* <ul>
43+
* <li>Time-based authorization rules</li>
44+
* <li>OAuth 2.0 scopes with expiration</li>
45+
* <li>Temporary elevated privileges</li>
46+
* </ul>
47+
*
48+
* <p>
49+
* Example usage: <pre>
50+
* GrantedAuthority authority = TimestampedGrantedAuthority.withAuthority("profile:read")
51+
* .issuedAt(Instant.now())
52+
* .expiresAt(Instant.now().plusSeconds(300))
53+
* .build();
54+
* </pre>
55+
*
56+
* @author Yoobin Yoon
57+
* @since 7.0
58+
*/
59+
public final class TimestampedGrantedAuthority implements GrantedAuthority {
60+
61+
private static final long serialVersionUID = 620L;
62+
63+
private final String authority;
64+
65+
private final Instant issuedAt;
66+
67+
private final @Nullable Instant notBefore;
68+
69+
private final @Nullable Instant expiresAt;
70+
71+
@SuppressWarnings("NullAway")
72+
private TimestampedGrantedAuthority(Builder builder) {
73+
this.authority = builder.authority;
74+
this.issuedAt = builder.issuedAt;
75+
this.notBefore = builder.notBefore;
76+
this.expiresAt = builder.expiresAt;
77+
}
78+
79+
/**
80+
* Creates a new {@link Builder} with the specified authority.
81+
* @param authority the authority value (must not be null or empty)
82+
* @return a new {@link Builder}
83+
*/
84+
public static Builder withAuthority(String authority) {
85+
return new Builder(authority);
86+
}
87+
88+
@Override
89+
public String getAuthority() {
90+
return this.authority;
91+
}
92+
93+
/**
94+
* Returns the instant when this authority was issued.
95+
* @return the issued-at instant
96+
*/
97+
public Instant getIssuedAt() {
98+
return this.issuedAt;
99+
}
100+
101+
/**
102+
* Returns the instant before which this authority is not valid.
103+
* @return the not-before instant, or {@code null} if not specified
104+
*/
105+
public @Nullable Instant getNotBefore() {
106+
return this.notBefore;
107+
}
108+
109+
/**
110+
* Returns the instant when this authority expires.
111+
* @return the expires-at instant, or {@code null} if not specified
112+
*/
113+
public @Nullable Instant getExpiresAt() {
114+
return this.expiresAt;
115+
}
116+
117+
@Override
118+
public boolean equals(Object obj) {
119+
if (this == obj) {
120+
return true;
121+
}
122+
if (obj instanceof TimestampedGrantedAuthority tga) {
123+
return this.authority.equals(tga.authority) && this.issuedAt.equals(tga.issuedAt)
124+
&& Objects.equals(this.notBefore, tga.notBefore) && Objects.equals(this.expiresAt, tga.expiresAt);
125+
}
126+
return false;
127+
}
128+
129+
@Override
130+
public int hashCode() {
131+
return Objects.hash(this.authority, this.issuedAt, this.notBefore, this.expiresAt);
132+
}
133+
134+
@Override
135+
public String toString() {
136+
StringBuilder sb = new StringBuilder();
137+
sb.append("TimestampedGrantedAuthority [");
138+
sb.append("authority=").append(this.authority);
139+
sb.append(", issuedAt=").append(this.issuedAt);
140+
if (this.notBefore != null) {
141+
sb.append(", notBefore=").append(this.notBefore);
142+
}
143+
if (this.expiresAt != null) {
144+
sb.append(", expiresAt=").append(this.expiresAt);
145+
}
146+
sb.append("]");
147+
return sb.toString();
148+
}
149+
150+
/**
151+
* Builder for {@link TimestampedGrantedAuthority}.
152+
*/
153+
public static final class Builder {
154+
155+
private final String authority;
156+
157+
private @Nullable Instant issuedAt;
158+
159+
private @Nullable Instant notBefore;
160+
161+
private @Nullable Instant expiresAt;
162+
163+
private Builder(String authority) {
164+
Assert.hasText(authority, "A granted authority textual representation is required");
165+
this.authority = authority;
166+
}
167+
168+
/**
169+
* Sets the instant when this authority was issued.
170+
* @param issuedAt the issued-at instant
171+
* @return this builder
172+
*/
173+
public Builder issuedAt(Instant issuedAt) {
174+
Assert.notNull(issuedAt, "issuedAt cannot be null");
175+
this.issuedAt = issuedAt;
176+
return this;
177+
}
178+
179+
/**
180+
* Sets the instant before which this authority is not valid.
181+
* @param notBefore the not-before instant
182+
* @return this builder
183+
*/
184+
public Builder notBefore(Instant notBefore) {
185+
Assert.notNull(notBefore, "notBefore cannot be null");
186+
this.notBefore = notBefore;
187+
return this;
188+
}
189+
190+
/**
191+
* Sets the instant when this authority expires.
192+
* @param expiresAt the expires-at instant
193+
* @return this builder
194+
*/
195+
public Builder expiresAt(Instant expiresAt) {
196+
Assert.notNull(expiresAt, "expiresAt cannot be null");
197+
this.expiresAt = expiresAt;
198+
return this;
199+
}
200+
201+
/**
202+
* Builds a new {@link TimestampedGrantedAuthority}.
203+
* <p>
204+
* If {@code issuedAt} is not set, it defaults to {@link Instant#now()}.
205+
* @return a new {@link TimestampedGrantedAuthority}
206+
* @throws IllegalArgumentException if temporal constraints are invalid
207+
*/
208+
public TimestampedGrantedAuthority build() {
209+
if (this.issuedAt == null) {
210+
this.issuedAt = Instant.now();
211+
}
212+
if (this.notBefore != null && this.notBefore.isBefore(this.issuedAt)) {
213+
throw new IllegalArgumentException("notBefore must not be before issuedAt");
214+
}
215+
if (this.expiresAt != null && this.expiresAt.isBefore(this.issuedAt)) {
216+
throw new IllegalArgumentException("expiresAt must not be before issuedAt");
217+
}
218+
if (this.notBefore != null && this.expiresAt != null && this.expiresAt.isBefore(this.notBefore)) {
219+
throw new IllegalArgumentException("expiresAt must not be before notBefore");
220+
}
221+
222+
return new TimestampedGrantedAuthority(this);
223+
}
224+
225+
}
226+
227+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 TimestampedGrantedAuthority}.
28+
*
29+
* @author Yoobin Yoon
30+
*/
31+
public class TimestampedGrantedAuthorityTests {
32+
33+
@Test
34+
public void buildWhenOnlyAuthorityThenDefaultsIssuedAtToNow() {
35+
Instant before = Instant.now();
36+
37+
TimestampedGrantedAuthority authority = TimestampedGrantedAuthority.withAuthority("profile:read").build();
38+
39+
Instant after = Instant.now();
40+
41+
assertThat(authority.getAuthority()).isEqualTo("profile:read");
42+
assertThat(authority.getIssuedAt()).isBetween(before, after);
43+
assertThat(authority.getNotBefore()).isNull();
44+
assertThat(authority.getExpiresAt()).isNull();
45+
}
46+
47+
@Test
48+
public void buildWhenAllFieldsSetThenCreatesCorrectly() {
49+
Instant issuedAt = Instant.now();
50+
Instant notBefore = issuedAt.plusSeconds(60);
51+
Instant expiresAt = issuedAt.plusSeconds(300);
52+
53+
TimestampedGrantedAuthority authority = TimestampedGrantedAuthority.withAuthority("admin:write")
54+
.issuedAt(issuedAt)
55+
.notBefore(notBefore)
56+
.expiresAt(expiresAt)
57+
.build();
58+
59+
assertThat(authority.getAuthority()).isEqualTo("admin:write");
60+
assertThat(authority.getIssuedAt()).isEqualTo(issuedAt);
61+
assertThat(authority.getNotBefore()).isEqualTo(notBefore);
62+
assertThat(authority.getExpiresAt()).isEqualTo(expiresAt);
63+
}
64+
65+
@Test
66+
public void buildWhenNullAuthorityThenThrowsException() {
67+
assertThatIllegalArgumentException().isThrownBy(() -> TimestampedGrantedAuthority.withAuthority(null))
68+
.withMessage("A granted authority textual representation is required");
69+
}
70+
71+
@Test
72+
public void buildWhenEmptyAuthorityThenThrowsException() {
73+
assertThatIllegalArgumentException().isThrownBy(() -> TimestampedGrantedAuthority.withAuthority(""))
74+
.withMessage("A granted authority textual representation is required");
75+
}
76+
77+
@Test
78+
public void buildWhenNotBeforeBeforeIssuedAtThenThrowsException() {
79+
Instant issuedAt = Instant.now();
80+
Instant notBefore = issuedAt.minusSeconds(60);
81+
82+
assertThatIllegalArgumentException().isThrownBy(
83+
() -> TimestampedGrantedAuthority.withAuthority("test").issuedAt(issuedAt).notBefore(notBefore).build())
84+
.withMessage("notBefore must not be before issuedAt");
85+
}
86+
87+
@Test
88+
public void buildWhenExpiresAtBeforeIssuedAtThenThrowsException() {
89+
Instant issuedAt = Instant.now();
90+
Instant expiresAt = issuedAt.minusSeconds(60);
91+
92+
assertThatIllegalArgumentException().isThrownBy(
93+
() -> TimestampedGrantedAuthority.withAuthority("test").issuedAt(issuedAt).expiresAt(expiresAt).build())
94+
.withMessage("expiresAt must not be before issuedAt");
95+
}
96+
97+
@Test
98+
public void buildWhenExpiresAtBeforeNotBeforeThenThrowsException() {
99+
Instant issuedAt = Instant.now();
100+
Instant notBefore = issuedAt.plusSeconds(60);
101+
Instant expiresAt = issuedAt.plusSeconds(30);
102+
103+
assertThatIllegalArgumentException()
104+
.isThrownBy(() -> TimestampedGrantedAuthority.withAuthority("test")
105+
.issuedAt(issuedAt)
106+
.notBefore(notBefore)
107+
.expiresAt(expiresAt)
108+
.build())
109+
.withMessage("expiresAt must not be before notBefore");
110+
}
111+
112+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /