View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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 }