Как создать токен доступа JWT с некоторыми пользовательскими утверждениями? - PullRequest
0 голосов
/ 11 января 2019

Я пытаюсь, чтобы мой сервер авторизации генерировал токен доступа JWT с некоторыми пользовательскими утверждениями в нем.

Вот как токен Bearer, возвращаемый сервером авторизации /auth/token, выглядит как конечная точка: 51aea31c-6b57-4c80-9d19-a72e15cb2bb7

Я нахожу этот токен немного коротким, чтобы быть токеном JWT и содержать мои пользовательские утверждения ...

И при использовании его в последующих запросах к серверу ресурсов выдает ошибку: Cannot convert access token to JSON

Я использую следующие зависимости:

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.1.RELEASE</version>
    <relativePath/>
  </parent>

  <dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
  </dependency>

Сервер авторизации настроен следующим образом:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  endpoints
  .tokenServices(defaultTokenServices())
  .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
  .accessTokenConverter(jwtAccessTokenConverter())
  .userDetailsService(userDetailsService);

  endpoints
  .pathMapping("/oauth/token", RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.TOKEN);

  TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
  tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
  endpoints
  .tokenStore(tokenStore())
  .tokenEnhancer(tokenEnhancerChain)
  .authenticationManager(authenticationManager);
}

@Bean
@Primary
public DefaultTokenServices defaultTokenServices() {
  DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
  defaultTokenServices.setTokenStore(tokenStore());
  defaultTokenServices.setSupportRefreshToken(true);
  return defaultTokenServices;
}

@Bean
public TokenStore tokenStore() {
  return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
  JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
  jwtAccessTokenConverter.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getSslKeystoreFilename()), jwtProperties.getSslKeystorePassword().toCharArray()).getKeyPair(jwtProperties.getSslKeyPair()));
return jwtAccessTokenConverter;
}

@Bean
public TokenEnhancer tokenEnhancer() {
  return new CustomTokenEnhancer();
}

И он использует класс:

class CustomTokenEnhancer implements TokenEnhancer {

  @Autowired
  private TokenAuthenticationService tokenAuthenticationService;

  // Add user information to the token
  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    User user = (User) authentication.getPrincipal();
    Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
    info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
    info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
    info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
    info.put("organization", authentication.getName());
    DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
    customAccessToken.setAdditionalInformation(info);
    customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
    return customAccessToken;
  }

}

У меня тоже есть класс:

@Configuration
class CustomOauth2RequestFactory extends DefaultOAuth2RequestFactory {

  @Autowired
  private TokenStore tokenStore;

  @Autowired
  private UserDetailsService userDetailsService;

  public CustomOauth2RequestFactory(ClientDetailsService clientDetailsService) {
    super(clientDetailsService);
  }

  @Override
  public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) {
    if (requestParameters.get("grant_type").equals("refresh_token")) {
      OAuth2Authentication authentication = tokenStore
          .readAuthenticationForRefreshToken(tokenStore.readRefreshToken(requestParameters.get("refresh_token")));
      SecurityContextHolder.getContext()
          .setAuthentication(new UsernamePasswordAuthenticationToken(authentication.getName(), null,
              userDetailsService.loadUserByUsername(authentication.getName()).getAuthorities()));
    }
    return super.createTokenRequest(requestParameters, authenticatedClient);
  }

}

ОБНОВЛЕНИЕ: я также попробовал альтернативный способ указания пользовательского утверждения:

@Component
class CustomAccessTokenConverter extends JwtAccessTokenConverter {

    @Autowired
  private TokenAuthenticationService tokenAuthenticationService;

  @Override
  public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
    OAuth2Authentication authentication = super.extractAuthentication(claims);
    authentication.setDetails(claims);
    return authentication;
  }

  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    User user = (User) authentication.getPrincipal();
    Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
    info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
    info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
    info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
    info.put("organization", authentication.getName());
    DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
    customAccessToken.setAdditionalInformation(info);
    customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
    return super.enhance(customAccessToken, authentication);
  }

}

с таким названием:

endpoints
.tokenStore(tokenStore())
.tokenEnhancer(jwtAccessTokenConverter())
.accessTokenConverter(jwtAccessTokenConverter())

но это ничего не изменило и ошибка осталась прежней.

При работе с отладчиком ни одно из этих двух переопределений энхансера не вызывается.

Ответы [ 2 ]

0 голосов
/ 18 января 2019

Чтобы построить сервер Spring Boot с OAuth2, JWT и дополнительными требованиями, нам нужно:

1) Добавить зависимость к проекту:

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

2) Добавить конфигурацию веб-безопасности (для публикации AuthenticationManager bean - она ​​будет использоваться на следующем шаге), например:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(username -> AuthUser.with()
                .username(username)
                .password("{noop}" + username)
                .email(username + "@mail.com")
                .authority(AuthUser.Role.values()[ThreadLocalRandom.current().nextInt(2)])
                .build()
        );
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

Здесь реализован простой UserDetailsService для целей тестирования. Он работает со следующим простым объектом 'User' и перечислением Role, которое реализует интерфейс GrantedAuthority. AuthUser имеет только одно дополнительное свойство email, которое будет добавлено в токен JWT в качестве заявки.

@Value
@EqualsAndHashCode(callSuper = false)
public class AuthUser extends User {

    private String email;

    @Builder(builderMethodName = "with")
    public AuthUser(final String username, final String password, @Singular final Collection<? extends GrantedAuthority> authorities, final String email) {
        super(username, password, authorities);
        this.email = email;
    }

    public enum Role implements GrantedAuthority {
        USER, ADMIN;

        @Override
        public String getAuthority() {
            return this.name();
        }
    }
}

3) Настройте сервер авторизации и включите сервер ресурсов:

@Configuration
@EnableAuthorizationServer
@EnableResourceServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    public static final String TOKEN_KEY = "abracadabra";

    private final AuthenticationManager authenticationManager;

    public AuthServerConfig(final AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsService) throws Exception {
        clientDetailsService.inMemory()
                .withClient("client")
                .secret("{noop}")
                .scopes("*")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(60 * 2) // 2 min
                .refreshTokenValiditySeconds(60 * 60); // 60 min
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain chain = new TokenEnhancerChain();
        chain.setTokenEnhancers(List.of(tokenEnhancer(), tokenConverter()));
        endpoints
                .tokenStore(tokenStore())
                .reuseRefreshTokens(false)
                .tokenEnhancer(chain)
                .authenticationManager(authenticationManager);
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(tokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter tokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(TOKEN_KEY);
        converter.setAccessTokenConverter(authExtractor());
        return converter;
    }

    private TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
                if (authentication != null && authentication.getPrincipal() instanceof AuthUser) {
                    AuthUser authUser = (AuthUser) authentication.getPrincipal();
                    Map<String, Object> additionalInfo = new HashMap<>();
                    additionalInfo.put("user_email", authUser.getEmail());
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
                }
                return accessToken;
            };
    }

    @Bean
    public DefaultAccessTokenConverter authExtractor() {
        return new DefaultAccessTokenConverter() {
            @Override
            public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
                OAuth2Authentication authentication = super.extractAuthentication(claims);
                authentication.setDetails(claims);
                return authentication;
            }
        };
    }
}

Простой ClientDetailsService реализован здесь. Он содержит только одного клиента с именем «client», пустым паролем и предоставленными типами «password» и «refresh_token». Это дает нам возможность создать новый токен доступа и обновить его. (Для работы со многими типами клиентов или в других сценариях вам необходимо реализовать более сложные и, возможно, постоянные варианты ClientDetailsService.)

Конечные точки авторизации конфигурируются с TokenEnhancerChain, который содержит tokenEnhancer и tokenConverter. Важно добавить их в этой последовательности. Первый дополняет токен доступа дополнительными претензиями (в нашем случае электронная почта пользователя). Второй создает токен JWT. Набор endpoints с простыми JwtTokenStore, нашими TokenEnhancerChain и authenticationManager.

Примечание к JwtTokenStore - Я считаю, что в реальных сценариях должен использоваться постоянный вариант магазина. Более подробная информация здесь .

Последняя вещь здесь authExtractor, которая дает нам возможность извлекать заявки из токенов JWT входящих запросов.

После того, как все настроено, мы можем запросить у нашего сервера токен доступа:

curl -i \
--user client: \
-H "Content-Type: application/x-www-form-urlencoded" \
-X POST \
-d "grant_type=password&username=user&password=user&scope=*" \
http://localhost:8080/oauth/token

Rsponse:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjoidXNlckBtYWlsLmNvbSIsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyIqIl0sImV4cCI6MTU0Nzc2NDIzOCwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiYzk1YzkzYTAtMThmOC00OGZjLWEzZGUtNWVmY2Y1YWIxMGE5IiwiY2xpZW50X2lkIjoiY2xpZW50In0.RWSGMC0w8tNafT28i2GLTnPnIiXfAlCdydEsNNZK-Lw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjoidXNlckBtYWlsLmNvbSIsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyIqIl0sImF0aSI6ImM5NWM5M2EwLTE4ZjgtNDhmYy1hM2RlLTVlZmNmNWFiMTBhOSIsImV4cCI6MTU0Nzc2NzcxOCwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiZDRhNGU2ZjUtNDY2Mi00NGZkLWI0ZDgtZWE5OWRkMDJkYWI2IiwiY2xpZW50X2lkIjoiY2xpZW50In0.m7XvxwuPiTnPaQXAptLfi3CxN3imfQCVKyjmMCIPAVM",
    "expires_in": 119,
    "scope": "*",
    "user_email": "user@mail.com",
    "jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9"
}

Если мы расшифруем этот токен доступа на https://jwt.io/, мы увидим, что он содержит утверждение user_email:

{
  "user_email": "user@mail.com",
  "user_name": "user",
  "scope": [
    "*"
  ],
  "exp": 1547764238,
  "authorities": [
    "ADMIN"
  ],
  "jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9",
  "client_id": "client"
}

Чтобы извлечь такую ​​заявку (и другие данные) из токена JWT входящих запросов, мы можем использовать следующий подход:

@RestController
public class DemoController {

    @GetMapping("/demo")
    public Map demo(OAuth2Authentication auth) {

        var details = (OAuth2AuthenticationDetails) auth.getDetails();
        //noinspection unchecked
        var decodedDetails = (Map<String, Object>) details.getDecodedDetails();

        return Map.of(
                "name", decodedDetails.get("user_name"),
                "email", decodedDetails.get("user_email"),
                "roles", decodedDetails.get("authorities")
        );
    }
}

Моя рабочая демонстрация: sb-jwt-oauth-demo

Информация, связанная с данной:

0 голосов
/ 15 января 2019

Если вы поделились примером проекта, было бы легче найти точное решение для вас. Вместо этого вы установили точку останова на .tokenEnhancer(tokenEnhancerChain) и она сработала?

Я создал очень простой пример проекта , который показывает, как tokenEnhancer вызывается

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean //by exposing this bean, password grant becomes enabled
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
            builder()
                .username("user")
                .password("{bcrypt}$2a$10$C8c78G3SRJpy268vInPUFu.3lcNHG9SaNAPdSaIOy.1TJIio0cmTK") //123
                .roles("USER")
                .build(),
            builder()
                .username("admin")
                .password("{bcrypt}$2a$10$XvWhl0acx2D2hvpOPd/rPuPA48nQGxOFom1NqhxNN9ST1p9lla3bG") //password
                .roles("ADMIN")
                .build()
        );
    }

    @EnableAuthorizationServer
    public static class Oauth2SecurityConfig extends AuthorizationServerConfigurerAdapter {
        private final PasswordEncoder passwordEncoder;
        private final AuthenticationManager authenticationManager;

        public Oauth2SecurityConfig(PasswordEncoder passwordEncoder,
                                    AuthenticationManager authenticationManager) {
            this.passwordEncoder = passwordEncoder;
            this.authenticationManager = authenticationManager;
        }

        @Bean
        public TokenEnhancer tokenEnhancer() {
            return new CustomTokenEnhancer();
        }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints
                .tokenEnhancer(tokenEnhancer())
                .authenticationManager(authenticationManager)
            ;

        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            InMemoryClientDetailsService clientDetails = new InMemoryClientDetailsService();
            BaseClientDetails client = new BaseClientDetails(
                "testclient",
                null,
                "testscope,USER,ADMIN",
                "password",
                null
            );
            client.setClientSecret(passwordEncoder.encode("secret"));
            clientDetails.setClientDetailsStore(
                Collections.singletonMap(
                    client.getClientId(),
                    client
                )
            );
            clients.withClientDetails(clientDetails);
        }

    }

}

В этом примере также есть юнит-тест

@Test
@DisplayName("perform a password grant")
void passwordGrant() throws Exception {
    mvc.perform(
        post("/oauth/token")
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
            .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .param("username", "admin")
            .param("password", "password")
            .param("grant_type", "password")
            .param("response_type", "token")
            .param("client_id", "testclient")
            .header("Authorization", "Basic "+ Base64.encodeBase64String("testclient:secret".getBytes()))
    )
        .andExpect(status().isOk())
        .andExpect(content().string(containsString("\"full_name\":\"Joe Schmoe\"")))
        .andExpect(content().string(containsString("\"email\":\"Joe@Schmoe.Com\"")))
    ;
}

Не стесняйтесь проверить пример проекта и посмотреть, работает ли он для вас.,

...