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 com.nimbusds.jose.JOSEException;
22  import com.nimbusds.jose.JWSAlgorithm;
23  import com.nimbusds.jose.JWSHeader;
24  import com.nimbusds.jose.jca.JCAContext;
25  import com.nimbusds.jose.util.Base64URL;
26  import com.nimbusds.jwt.JWTClaimsSet;
27  import java.time.Duration;
28  import java.time.Instant;
29  import java.time.OffsetDateTime;
30  import java.time.ZoneOffset;
31  import java.util.Set;
32  import org.apache.commons.lang3.tuple.Pair;
33  import org.apache.syncope.core.persistence.api.dao.UserDAO;
34  import org.apache.syncope.core.persistence.api.entity.user.User;
35  import org.apache.syncope.core.spring.security.jws.MSEntraAccessTokenJWSVerifier;
36  import org.springframework.transaction.annotation.Transactional;
37  
38  /**
39   * JWT authorisation for access tokens issued by Microsoft Entra (formerly Azure)
40   * for Microsoft Entra-only applications (v1.0 tokens)
41   * cf. https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens
42   */
43  public class MSEntraJWTSSOProvider implements JWTSSOProvider {
44  
45      protected final UserDAO userDAO;
46  
47      protected final AuthDataAccessor authDataAccessor;
48  
49      protected final String tenantId;
50  
51      protected final String appId;
52  
53      protected final String authUsername;
54  
55      protected final Duration clockSkew;
56  
57      protected final MSEntraAccessTokenJWSVerifier verifier;
58  
59      public MSEntraJWTSSOProvider(
60              final UserDAO userDAO,
61              final AuthDataAccessor authDataAccessor,
62              final String tenantId,
63              final String appId,
64              final String authUsername,
65              final Duration clockSkew,
66              final MSEntraAccessTokenJWSVerifier verifier) {
67  
68          this.userDAO = userDAO;
69          this.authDataAccessor = authDataAccessor;
70          this.tenantId = tenantId;
71          this.appId = appId;
72          this.authUsername = authUsername;
73          this.clockSkew = clockSkew;
74          this.verifier = verifier;
75      }
76  
77      @Override
78      public String getIssuer() {
79          return String.format("https://sts.windows.net/%s/", tenantId);
80      }
81  
82      @Override
83      public Set<JWSAlgorithm> supportedJWSAlgorithms() {
84          return verifier.supportedJWSAlgorithms();
85      }
86  
87      @Override
88      public JCAContext getJCAContext() {
89          return verifier.getJCAContext();
90      }
91  
92      /*
93       * When parsing the token, you must [...] ensure the token meets these requirements:
94       *
95       * - The token was sent in the HTTP Authorization header with "Bearer" scheme.
96       * - The token is valid JSON that conforms to the JWT standard.
97       * - The token contains an "issuer" claim with one of the highlighted values for non-governmental cases.
98       * - The token contains an "audience" claim with a value equal to the Microsoft App ID.
99       * - The token is within its validity period. Industry-standard clock-skew is 5 minutes.
100      * - The token has a valid cryptographic signature with a key listed in the OpenID keys document that was retrieved
101      * from the `jwks_uri` property in the OpenID metadata document via GET request.
102      *
103      * cf. https://learn.microsoft.com/en-us/entra/identity-platform/security-tokens#validate-security-tokens
104      */
105     @Override
106     public boolean verify(final JWSHeader header, final byte[] signingInput, final Base64URL signature)
107             throws JOSEException {
108 
109         return verifier.verify(header, signingInput, signature);
110     }
111 
112     @Transactional(readOnly = true)
113     @Override
114     public Pair<User, Set<SyncopeGrantedAuthority>> resolve(final JWTClaimsSet jwtClaims) {
115         User authUser = userDAO.findByUsername(authUsername);
116         Set<SyncopeGrantedAuthority> authorities = Set.of();
117 
118         Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
119         Instant issued = jwtClaims.getIssueTime().toInstant();
120         Instant notBefore = jwtClaims.getNotBeforeTime().toInstant();
121         Instant expired = jwtClaims.getExpirationTime().toInstant();
122 
123         if (authUser != null
124                 && jwtClaims.getAudience().contains(appId)
125                 && now.isAfter(issued.minus(clockSkew))
126                 && now.isAfter(notBefore.minus(clockSkew))
127                 && now.isBefore(expired.plus(clockSkew))) {
128 
129             authorities = authDataAccessor.getAuthorities(authUser.getUsername(), null);
130         }
131 
132         return Pair.of(authUser, authorities);
133     }
134 }