Я пытаюсь реализовать внутреннюю сторону аутентификации OpenId Connect. Это API без сохранения состояния, поэтому я добавил фильтр, который обрабатывает токен Bearer.
Я создал фильтр OpenIdConnect, который обрабатывает аутентификацию, и добавил его в WebSecurityConfigurerAdapter.
public class OpenIdConnectFilter extends
AbstractAuthenticationProcessingFilter {
@Value("${auth0.clientId}")
private String clientId;
@Value("${auth0.issuer}")
private String issuer;
@Value("${auth0.keyUrl}")
private String jwkUrl;
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
public OpenIdConnectFilter() {
super("/connect/**");
setAuthenticationManager(new NoopAuthenticationManager());
}
@Bean
public FilterRegistrationBean registration(OpenIdConnectFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
try {
Authentication authentication = tokenExtractor.extract(request);
String accessToken = (String) authentication.getPrincipal();
String kid = JwtHelper.headers(accessToken)
.get("kid");
final Jwt tokenDecoded = JwtHelper.decodeAndVerify(accessToken, verifier(kid));
final Map<String, Object> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
Set<String> scopes = new HashSet<String>(Arrays.asList(((String) authInfo.get("scope")).split(" ")));
int expires = (Integer) authInfo.get("exp");
OpenIdToken openIdToken = new OpenIdToken(accessToken, scopes, Long.valueOf(expires), authInfo);
final OpenIdUserDetails user = new OpenIdUserDetails((String) authInfo.get("sub"), "Test", openIdToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (final Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) || !claims.get("azp").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}
private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
Вот конфигурация безопасности:
@Configuration
@EnableWebSecurity
public class OpenIdConnectWebServerConfig extends
WebSecurityConfigurerAdapter {
@Bean
public OpenIdConnectFilter myFilter() {
final OpenIdConnectFilter filter = new OpenIdConnectFilter();
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.antMatcher("/connect/**").authorizeRequests()
.antMatchers(HttpMethod.GET, "/connect/public").permitAll()
.antMatchers(HttpMethod.GET, "/connect/private").authenticated()
.antMatchers(HttpMethod.GET, "/connect/private-
messages").hasAuthority("read:messages")
.antMatchers(HttpMethod.GET, "/connect/private-
roles").hasAuthority("read:roles")
.and()
.addFilterBefore(myFilter(),
UsernamePasswordAuthenticationFilter.class);
}
Конечные точки отдыха выглядят следующим образом:
@RequestMapping(value = "/connect/public", method = RequestMethod.GET,
produces = "application/json")
@ResponseBody
public String publicEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You DO NOT need to be authenticated to
call /api/public.")
.toString();
}
@RequestMapping(value = "/connect/private", method = RequestMethod.GET,
produces = "application/json")
@ResponseBody
public String privateEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You can see this because you are
Authenticated.")
.toString();
}
Если я полностью удаляю фильтр для конфигурации, а также определение @Bean, конфигурация работает, как ожидается: / connect / public доступна, тогда как / connect / private запрещена.
Если я сохраню определение @Bean и добавлю его в цепочку фильтров, ответ вернет статус Not Found для запросов как в / connect / public, так и / connect / private:
"timestamp": "18.01.2019 09:46:11",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/
При отладке я заметил, что фильтр обрабатывает токен и возвращает реализацию Аутентификации.
- Правильно ли добавлен фильтр в цепочку фильтров и в правильном ли положении?
Почему фильтр также вызывается по пути / connect / public, если он должен быть открытым. Применяется ли он ко всем путям, соответствующим вызову super ("/ connect / **")?
Почему он возвращает путь как "/", когда запрос сделан в / connect / private
Кажется, что-то не так с фильтром, потому что каждый раз, когда он применяется, ответ портится.