1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.core.spring.security;
20
21 import static org.junit.jupiter.api.Assertions.assertEquals;
22 import static org.junit.jupiter.api.Assertions.assertNull;
23 import static org.junit.jupiter.api.Assertions.assertThrows;
24 import static org.junit.jupiter.api.Assertions.assertTrue;
25 import static org.mockito.ArgumentMatchers.anyString;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.when;
28
29 import com.nimbusds.jwt.JWTClaimsSet;
30 import java.time.Duration;
31 import java.time.Instant;
32 import java.time.OffsetDateTime;
33 import java.time.ZoneOffset;
34 import java.time.temporal.ChronoUnit;
35 import java.util.Date;
36 import java.util.Set;
37 import org.apache.commons.lang3.tuple.Pair;
38 import org.apache.syncope.core.persistence.api.dao.UserDAO;
39 import org.apache.syncope.core.persistence.api.entity.user.User;
40 import org.apache.syncope.core.spring.security.jws.MSEntraAccessTokenJWSVerifier;
41 import org.junit.jupiter.api.Test;
42 import org.junit.jupiter.api.extension.ExtendWith;
43 import org.mockito.Mock;
44 import org.mockito.junit.jupiter.MockitoExtension;
45
46 @ExtendWith(MockitoExtension.class)
47 public class MSEntraJWTSSOProviderTest {
48
49 private static final String TENANT_ID = "test-tenant-id";
50
51 private static final String APP_ID = "test-app-id";
52
53 private static final String AUTH_USERNAME = "auth-username";
54
55 private static final MSEntraAccessTokenJWSVerifier VERIFIER = new MSEntraAccessTokenJWSVerifier(
56 TENANT_ID, APP_ID, Duration.ofHours(24));
57
58 @Mock
59 private User user;
60
61 @Mock
62 private UserDAO userDAO;
63
64 @Mock
65 private AuthDataAccessor authDataAccessor;
66
67 @Test
68 void getIssuer() {
69 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
70 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
71
72 assertEquals(provider.getIssuer(), "https://sts.windows.net/" + TENANT_ID + "/");
73 }
74
75 @Test
76 void resolveSuccess() {
77 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
78 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
79
80 when(userDAO.findByUsername(anyString())).thenReturn(user);
81 when(authDataAccessor.getAuthorities(AUTH_USERNAME, null)).
82 thenReturn(Set.of(mock(SyncopeGrantedAuthority.class)));
83 when(user.getUsername()).thenReturn(AUTH_USERNAME);
84
85 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
86 Instant issued = now.minus(65, ChronoUnit.SECONDS);
87 Instant notBefore = now.minus(5, ChronoUnit.SECONDS);
88 Instant expiration = now.plus(1, ChronoUnit.HOURS);
89
90 JWTClaimsSet payload = new JWTClaimsSet.Builder()
91 .issuer(TENANT_ID)
92 .audience(APP_ID)
93 .issueTime(Date.from(issued))
94 .notBeforeTime(Date.from(notBefore))
95 .expirationTime(Date.from(expiration))
96 .build();
97
98 Pair<User, Set<SyncopeGrantedAuthority>> resolved = provider.resolve(payload);
99 assertEquals(AUTH_USERNAME, resolved.getKey().getUsername());
100 assertEquals(1, resolved.getValue().size());
101 }
102
103 @Test
104 void resolveMissingClaims() {
105 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
106 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
107
108 when(userDAO.findByUsername(anyString())).thenReturn(user);
109
110 JWTClaimsSet payload = new JWTClaimsSet.Builder()
111 .issuer(TENANT_ID)
112 .audience(APP_ID)
113 .build();
114
115 assertThrows(Exception.class, () -> provider.resolve(payload));
116 }
117
118 @Test
119 void resolveAuthUserNull() {
120 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
121 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
122
123 when(userDAO.findByUsername(anyString())).thenReturn(null);
124
125 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
126 Instant issued = now.minus(1, ChronoUnit.MINUTES);
127 Instant notBefore = now.minus(1, ChronoUnit.SECONDS);
128 Instant expiration = now.plus(59, ChronoUnit.MINUTES);
129
130 JWTClaimsSet payload = new JWTClaimsSet.Builder()
131 .issuer(TENANT_ID)
132 .audience(APP_ID)
133 .issueTime(Date.from(issued))
134 .notBeforeTime(Date.from(notBefore))
135 .expirationTime(Date.from(expiration))
136 .build();
137
138 Pair<User, Set<SyncopeGrantedAuthority>> resolved = provider.resolve(payload);
139 assertNull(resolved.getKey());
140 assertTrue(resolved.getValue().isEmpty());
141 }
142
143 @Test
144 void resolveWrongAudience() {
145 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
146 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
147
148 when(userDAO.findByUsername(anyString())).thenReturn(user);
149
150 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
151 Instant issued = now.minus(1, ChronoUnit.MINUTES);
152 Instant notBefore = now.minus(1, ChronoUnit.SECONDS);
153 Instant expiration = now.plus(59, ChronoUnit.MINUTES);
154
155 JWTClaimsSet payload = new JWTClaimsSet.Builder()
156 .issuer(TENANT_ID)
157 .audience("wrong-audience-claim")
158 .issueTime(Date.from(issued))
159 .notBeforeTime(Date.from(notBefore))
160 .expirationTime(Date.from(expiration))
161 .build();
162
163 assertTrue(provider.resolve(payload).getValue().isEmpty());
164 }
165
166 @Test
167 void resolveIssuedFail() {
168 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
169 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
170
171 when(userDAO.findByUsername(anyString())).thenReturn(user);
172
173 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
174 Instant issued = now.plus(6, ChronoUnit.MINUTES);
175 Instant notBefore = now.minus(5, ChronoUnit.SECONDS);
176 Instant expiration = now.plus(1, ChronoUnit.HOURS);
177
178 JWTClaimsSet payload = new JWTClaimsSet.Builder()
179 .issuer(TENANT_ID)
180 .audience(APP_ID)
181 .issueTime(Date.from(issued))
182 .notBeforeTime(Date.from(notBefore))
183 .expirationTime(Date.from(expiration))
184 .build();
185
186 assertTrue(provider.resolve(payload).getValue().isEmpty());
187 }
188
189 @Test
190 void resolveIssuedInClockSkew() {
191 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
192 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
193
194 when(userDAO.findByUsername(anyString())).thenReturn(user);
195 when(authDataAccessor.getAuthorities(AUTH_USERNAME, null)).
196 thenReturn(Set.of(mock(SyncopeGrantedAuthority.class)));
197 when(user.getUsername()).thenReturn(AUTH_USERNAME);
198
199 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
200 Instant issued = now.plus(4, ChronoUnit.MINUTES);
201 Instant notBefore = now.minus(5, ChronoUnit.SECONDS);
202 Instant expiration = now.plus(1, ChronoUnit.HOURS);
203
204 JWTClaimsSet payload = new JWTClaimsSet.Builder()
205 .issuer(TENANT_ID)
206 .audience(APP_ID)
207 .issueTime(Date.from(issued))
208 .notBeforeTime(Date.from(notBefore))
209 .expirationTime(Date.from(expiration))
210 .build();
211
212 assertEquals(1, provider.resolve(payload).getValue().size());
213 }
214
215 @Test
216 void resolveNotBeforeFail() {
217 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
218 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
219
220 when(userDAO.findByUsername(anyString())).thenReturn(user);
221
222 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
223 Instant issued = now.minus(1, ChronoUnit.MINUTES);
224 Instant notBefore = now.plus(6, ChronoUnit.MINUTES);
225 Instant expiration = now.plus(1, ChronoUnit.HOURS);
226
227 JWTClaimsSet payload = new JWTClaimsSet.Builder()
228 .issuer(TENANT_ID)
229 .audience(APP_ID)
230 .issueTime(Date.from(issued))
231 .notBeforeTime(Date.from(notBefore))
232 .expirationTime(Date.from(expiration))
233 .build();
234
235 assertTrue(provider.resolve(payload).getValue().isEmpty());
236 }
237
238 @Test
239 void resolveNotBeforeInClockSkew() {
240 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
241 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
242
243 when(userDAO.findByUsername(anyString())).thenReturn(user);
244 when(authDataAccessor.getAuthorities(AUTH_USERNAME, null)).
245 thenReturn(Set.of(mock(SyncopeGrantedAuthority.class)));
246 when(user.getUsername()).thenReturn(AUTH_USERNAME);
247
248 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
249 Instant issued = now.minus(1, ChronoUnit.MINUTES);
250 Instant notBefore = now.plus(4, ChronoUnit.MINUTES);
251 Instant expiration = now.plus(1, ChronoUnit.HOURS);
252
253 JWTClaimsSet payload = new JWTClaimsSet.Builder()
254 .issuer(TENANT_ID)
255 .audience(APP_ID)
256 .issueTime(Date.from(issued))
257 .notBeforeTime(Date.from(notBefore))
258 .expirationTime(Date.from(expiration))
259 .build();
260
261 assertEquals(1, provider.resolve(payload).getValue().size());
262 }
263
264 @Test
265 void resolveExpirationFail() {
266 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
267 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
268
269 when(userDAO.findByUsername(anyString())).thenReturn(user);
270
271 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
272 Instant issued = now.minus(1, ChronoUnit.HOURS);
273 Instant notBefore = now.minus(1, ChronoUnit.HOURS);
274 Instant expiration = now.minus(6, ChronoUnit.MINUTES);
275
276 JWTClaimsSet payload = new JWTClaimsSet.Builder()
277 .issuer(TENANT_ID)
278 .audience(APP_ID)
279 .issueTime(Date.from(issued))
280 .notBeforeTime(Date.from(notBefore))
281 .expirationTime(Date.from(expiration))
282 .build();
283
284 assertTrue(provider.resolve(payload).getValue().isEmpty());
285 }
286
287 @Test
288 void resolveExpirationInClockSkew() {
289 MSEntraJWTSSOProvider provider = new MSEntraJWTSSOProvider(
290 userDAO, authDataAccessor, TENANT_ID, APP_ID, AUTH_USERNAME, Duration.ofMinutes(5), VERIFIER);
291
292 when(userDAO.findByUsername(anyString())).thenReturn(user);
293 when(authDataAccessor.getAuthorities(AUTH_USERNAME, null)).
294 thenReturn(Set.of(mock(SyncopeGrantedAuthority.class)));
295 when(user.getUsername()).thenReturn(AUTH_USERNAME);
296
297 Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
298 Instant issued = now.minus(1, ChronoUnit.HOURS);
299 Instant notBefore = now.minus(1, ChronoUnit.HOURS);
300 Instant expiration = now.minus(4, ChronoUnit.MINUTES);
301
302 JWTClaimsSet payload = new JWTClaimsSet.Builder()
303 .issuer(TENANT_ID)
304 .audience(APP_ID)
305 .issueTime(Date.from(issued))
306 .notBeforeTime(Date.from(notBefore))
307 .expirationTime(Date.from(expiration))
308 .build();
309
310 assertEquals(1, provider.resolve(payload).getValue().size());
311 }
312 }