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.jwt.JWTClaimsSet;
23  import com.nimbusds.jwt.SignedJWT;
24  import java.io.IOException;
25  import java.text.ParseException;
26  import java.util.Date;
27  import java.util.Optional;
28  import java.util.Set;
29  import javax.servlet.FilterChain;
30  import javax.servlet.ServletException;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  import org.apache.commons.lang3.tuple.Pair;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  import org.springframework.http.HttpHeaders;
37  import org.springframework.security.authentication.AuthenticationManager;
38  import org.springframework.security.authentication.BadCredentialsException;
39  import org.springframework.security.authentication.CredentialsExpiredException;
40  import org.springframework.security.core.AuthenticationException;
41  import org.springframework.security.core.context.SecurityContextHolder;
42  import org.springframework.security.web.AuthenticationEntryPoint;
43  import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
44  
45  /**
46   * Processes the JSON Web Token provided as {@link HttpHeaders#AUTHORIZATION} HTTP header, putting the result into the
47   * {@link SecurityContextHolder}.
48   */
49  public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
50  
51      private static final Logger LOG = LoggerFactory.getLogger(JWTAuthenticationFilter.class);
52  
53      private final AuthenticationEntryPoint authenticationEntryPoint;
54  
55      private final SyncopeAuthenticationDetailsSource authenticationDetailsSource;
56  
57      private final AuthDataAccessor dataAccessor;
58  
59      private final DefaultCredentialChecker credentialChecker;
60  
61      public JWTAuthenticationFilter(
62              final AuthenticationManager authenticationManager,
63              final AuthenticationEntryPoint authenticationEntryPoint,
64              final SyncopeAuthenticationDetailsSource authenticationDetailsSource,
65              final AuthDataAccessor dataAccessor,
66              final DefaultCredentialChecker credentialChecker) {
67  
68          super(authenticationManager);
69          this.authenticationEntryPoint = authenticationEntryPoint;
70          this.authenticationDetailsSource = authenticationDetailsSource;
71          this.dataAccessor = dataAccessor;
72          this.credentialChecker = credentialChecker;
73      }
74  
75      @Override
76      protected void doFilterInternal(
77              final HttpServletRequest request,
78              final HttpServletResponse response,
79              final FilterChain chain)
80              throws ServletException, IOException {
81  
82          String auth = request.getHeader(HttpHeaders.AUTHORIZATION);
83          String[] parts = Optional.ofNullable(auth).map(s -> s.split(" ")).orElse(null);
84          if (parts == null || parts.length != 2 || !"Bearer".equals(parts[0])) {
85              chain.doFilter(request, response);
86              return;
87          }
88  
89          String stringToken = parts[1];
90          LOG.debug("JWT received: {}", stringToken);
91  
92          try {
93              credentialChecker.checkIsDefaultJWSKeyInUse();
94  
95              // 0. parse JWT
96              SignedJWT jwt = SignedJWT.parse(stringToken);
97  
98              // 1. check signature
99              JWTSSOProvider jwtSSOProvider = dataAccessor.getJWTSSOProvider(jwt.getJWTClaimsSet().getIssuer());
100             if (!jwt.verify(jwtSSOProvider)) {
101                 throw new BadCredentialsException("Invalid signature found in JWT");
102             }
103 
104             JWTClaimsSet claims = jwt.getJWTClaimsSet();
105             long referenceTime = System.currentTimeMillis();
106 
107             // 2. check expiration
108             Date expirationTime = claims.getExpirationTime();
109             if (expirationTime != null && expirationTime.getTime() < referenceTime) {
110                 dataAccessor.removeExpired(claims.getJWTID());
111                 throw new CredentialsExpiredException("JWT is expired");
112             }
113 
114             // 3. check not before
115             Date notBefore = claims.getNotBeforeTime();
116             if (notBefore != null && notBefore.getTime() > referenceTime) {
117                 throw new CredentialsExpiredException("JWT not valid yet");
118             }
119 
120             // 4. generate and set the authentication object
121             JWTAuthentication jwtAuthentication =
122                     new JWTAuthentication(claims, authenticationDetailsSource.buildDetails(request));
123             jwtAuthentication.setAuthenticated(true);
124             AuthContextUtils.callAsAdmin(jwtAuthentication.getDetails().getDomain(), () -> {
125                 Pair<String, Set<SyncopeGrantedAuthority>> authenticated = dataAccessor.authenticate(jwtAuthentication);
126                 jwtAuthentication.setUsername(authenticated.getLeft());
127                 jwtAuthentication.getAuthorities().addAll(authenticated.getRight());
128                 return null;
129             });
130             SecurityContextHolder.getContext().setAuthentication(jwtAuthentication);
131 
132             chain.doFilter(request, response);
133         } catch (ParseException | JOSEException e) {
134             SecurityContextHolder.clearContext();
135             this.authenticationEntryPoint.commence(
136                     request, response, new BadCredentialsException("Invalid JWT: " + stringToken, e));
137         } catch (AuthenticationException e) {
138             SecurityContextHolder.clearContext();
139             this.authenticationEntryPoint.commence(request, response, e);
140         }
141     }
142 }