java – Oauth2: Invalid access token – Stack Overflow

Config from application.yml

We support multiple authorization servers, here is the fully configured azure client:

Oauth2, spring авторизация через vk – ошибка invalid_token_response tokentype cannot be null

Потратив почти весь день закопавшись в глубь sprign разбираясь что же не так, я нашел в чем была проблема, надеюсь эта информация кому то сэкономит кучу времени и нервных клеток.

Оказывается Vk в ответе получения токена не возвращается тип токена token_type Bearer, от сюда и срабатывает исключение “tokenType cannot be null”.
Но это часть проблемы, также Vk в месте с токеном возвращает email пользователя, плюс на запрос информации о пользователе по токену, ответ зачем то заворачивается в дополнительное поле “response” из за этого в OAuth2UserService при попытке получить Map срабатывает исключение. Запрос и разбор ответа пришлось делать вручную.

Для решения этой проблемы мне очень помогли статьи мною глубоко уважаемого Вaeldung.

а также серия статей – авторизация пользователей через социальные сети
https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-1/

1.Oбработчик ответа получения токена.

public class CustomTokenResponseConverter implements Converter<Map<String, String>, OAuth2AccessTokenResponse> {

  @Override
  public OAuth2AccessTokenResponse convert(Map<String, String> tokenResponseParameters) {
    String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
    long expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));


    OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER;

    Map<String, Object> additionalParameters = new HashMap<>();

    tokenResponseParameters.forEach((s, s2) -> {
      additionalParameters.put(s, s2);
    });

    return OAuth2AccessTokenResponse.withToken(accessToken)
            .tokenType(accessTokenType)
            .expiresIn(expiresIn)
            .additionalParameters(additionalParameters)
            .build();
  }
}

тут и происходит вся магия, вот эта строчка – OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER;

2.Настраиваем клиента получения информации о пользователе по токену.
ClientRegistration

  public static ClientRegistration getVk() {
    ClientRegistration.Builder builder = getBuilder("vk", ClientAuthenticationMethod.POST, "{baseUrl}/{action}/oauth2/code/{registrationId}");
    builder.scope("xxxxx");
    builder.authorizationUri("https://oauth.vk.com/authorize?v=5.95");
    builder.tokenUri("https://oauth.vk.com/access_token");
    builder.userInfoUri("https://api.vk.com/method/users.get?{user_id}&v=5.95&fields=photo_id,verified,sex,bdate,city,country,photo_max,home_town,has_photo&display=popup&lang=ru&access_token=xxxxx");
    builder.clientName("vkontakte");
    builder.redirectUriTemplate("{baseUrl}/oauth2/callback/{registrationId}");
    builder.clientId("xxxxx");
    builder.clientSecret("xxxx");
    builder.userNameAttributeName("user_id");
    builder.registrationId("vk");
    return builder.build();
  }

3.Настраиваем CustomOAuth2UserService от DefaultOAuth2UserService где запрашивается информация о пользователе.

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

  @Override
  public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {

    OAuth2User oAuth2User;
    if (oAuth2UserRequest.getClientRegistration().getRegistrationId().equals("vk")) {
      oAuth2User = loadVkUser(oAuth2UserRequest);
    } else {
      oAuth2User = super.loadUser(oAuth2UserRequest);
    }

    try {
      return processOAuth2User(oAuth2UserRequest, oAuth2User);
    } catch (AuthenticationException ex) {
      throw new AuthException(ex.getMessage());
    } catch (Exception ex) {
      // Throwing an instance of AuthenticationException will trigger the OAuth2AuthenticationFailureHandler
      throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
    }
  }

  private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
    OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes());

    .....
    return CustomUserPrincipal.create(socialUser, oAuth2User.getAttributes());
  }


  private OAuth2User loadVkUser(OAuth2UserRequest oAuth2UserRequest) {
    RestTemplate template = new RestTemplate();

    MultiValueMap<String, String> headers = new LinkedMultiValueMap();
    headers.add("Content-Type", "application/json");
    headers.add("Authorization", oAuth2UserRequest.getAccessToken().getTokenType().getValue()   " "   oAuth2UserRequest.getAccessToken().getTokenValue());
    HttpEntity<?> httpRequest = new HttpEntity(headers);
    String uri = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();
    String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
    uri = uri.replace("{user_id}", userNameAttributeName   "="   oAuth2UserRequest.getAdditionalParameters().get(userNameAttributeName));

    try {
      ResponseEntity<Object> entity = template.exchange(uri, HttpMethod.GET, httpRequest, Object.class);
      Map<String, Object> response = (Map) entity.getBody();
      ArrayList valueList = (ArrayList) response.get("response");
      Map<String, Object> userAttributes = (Map<String, Object>) valueList.get(0);
      userAttributes.put(userNameAttributeName, oAuth2UserRequest.getAdditionalParameters().get(userNameAttributeName));      
      ......

      Set<GrantedAuthority> authorities = Collections.singleton(new OAuth2UserAuthority(userAttributes));
      return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);

    } catch (HttpClientErrorException ex) {
      ex.printStackTrace();
      throw new BaseRuntimeException(ex.getMessage(), HttpStatus.UNAUTHORIZED);
    }
  }
}

4.Все классы инициализируем в WebSecurityConfigurerAdapter.

public class SecurityConfig extends WebSecurityConfigurerAdapter {

  private static List<String> clients = Arrays.asList("google", "vk", "facebook", "ok", "yandex");

  @Autowired
  private CustomUserDetailsService customUserDetailsService;

  @Autowired
  private CustomOAuth2UserService customOAuth2UserService;

  @Autowired
  private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

  @Autowired
  private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;

  ....


  @Bean
  protected CustomClientRegistration customClientRegistration() {
    return new CustomClientRegistration();
  }

  @Bean
  protected ClientRegistrationRepository clientRegistrationRepository() {
    List<ClientRegistration> registrations = clients.stream()
        .map(c -> customClientRegistration().getRegistration(c))
        .filter(registration -> registration != null)
        .collect(Collectors.toList());

    return new InMemoryClientRegistrationRepository(registrations);
  }  

  @Override
  protected void configure(final HttpSecurity http) throws Exception {
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .anyRequest().authenticated()
        .and().formLogin().permitAll()
        .and().csrf().disable()
        .formLogin().disable()
        .httpBasic().disable()
        .exceptionHandling().authenticationEntryPoint(new AuthExceptionEntryPoint())
        .and()
        .oauth2Login()
        .clientRegistrationRepository(clientRegistrationRepository())
        .authorizationEndpoint().authorizationRequestResolver(new CustomAuthorizationRequestResolver(clientRegistrationRepository(), "/oauth2/authorize"))
        .authorizationRequestRepository(cookieAuthorizationRequestRepository())
        .and()
        .redirectionEndpoint()
        .baseUri("/oauth2/callback/*")
        .and()
        .userInfoEndpoint()
        .userService(customOAuth2UserService)
        .and()
        .successHandler(oAuth2AuthenticationSuccessHandler)
        .failureHandler(oAuth2AuthenticationFailureHandler);

    // Add our custom Token based authentication filter
    http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    http.oauth2Login().tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient());
  }

  @Bean
  public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient(){
    DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
            new DefaultAuthorizationCodeTokenResponseClient();
    accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter());

    OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
            new OAuth2AccessTokenResponseHttpMessageConverter();
    tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter());

    RestTemplate restTemplate = new RestTemplate(Arrays.asList(
            new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
    restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

    accessTokenResponseClient.setRestOperations(restTemplate);
    return accessTokenResponseClient;
  }
 }

Oauth2: invalid access token

I have a web application which implements oauth2 and spring security. Also I have a mobile version of my web application which will access some protected resources of my web application. When I access my token url, it gives the access token ,refresh token, token_type, expires_in. I have a method which I want the android to have access to.

So this is the url to get an access token:

http://localhost:8080/LEAVE/oauth/token?scope=read,write,trust&grant_type=password&client_id=testclient&client_secret=testsecret&username=john&password=smith

And it gives me this:

{
    "access_token": "23ac9377-6de7-47b7-aab9-8aebc9c499d4",
    "token_type": "bearer",
    "refresh_token": "e8e0238c-a98e-4be3-93a9-fbe24bcf6e1d",
    "expires_in": 119,
    "scope": "read,write,trust"
}

And now to access the protected resource:

http://localhost:8080/LEAVE/api/users?access_token=23ac9377-6de7-47b7-aab9-8aebc9c499d4

When I call this url , it gives me the following error:

{
    "error": "invalid_token",
    "error_description": "Invalid access token: 23ac9377-6de7-47b7-aab9-8 aebc9c499d4"
}

The class that is protected:

@Scope("session")
@RequestMapping("/api/users")
@Component("ParParkingRequestComponent")
public class LeaveComponentImpl implements LeaveComponent {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    @ResponseBody
    public String test(){
        return "Yes It's working";
    }

Here is my spring-security.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 
                    http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd
                    http://www.springframework.org/schema/beans
                    http://www.springframework.org/schema/beans/spring-beans.xsd
                    http://www.springframework.org/schema/security
                    http://www.springframework.org/schema/security/spring-security-3.1.xsd">


<!-- This is default url to get a token from OAuth -->
<http pattern="/oauth/token" create-session="stateless"
    authentication-manager-ref="clientAuthenticationManager"
    xmlns="http://www.springframework.org/schema/security">
    <intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY" />
    <anonymous enabled="false" />
    <http-basic entry-point-ref="clientAuthenticationEntryPoint" />
    <!-- include this only if you need to authenticate clients via request 
        parameters -->
    <custom-filter ref="clientCredentialsTokenEndpointFilter"
        after="BASIC_AUTH_FILTER" />
    <access-denied-handler ref="oauthAccessDeniedHandler" />
</http>


<!-- This is where we tells spring security what URL should be protected 
    and what roles have access to them -->
<http pattern="/api/**" create-session="never"
    entry-point-ref="oauthAuthenticationEntryPoint"
    access-decision-manager-ref="accessDecisionManager"
    xmlns="http://www.springframework.org/schema/security">
    <anonymous enabled="false" />
    <intercept-url pattern="/api/**" access="ROLE_APP" />
    <custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
    <access-denied-handler ref="oauthAccessDeniedHandler" />
</http>

<bean id="oauthAuthenticationEntryPoint"
    class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    <property name="realmName" value="test" />
</bean>

<bean id="clientAuthenticationEntryPoint"
    class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    <property name="realmName" value="test/client" />
    <property name="typeName" value="Basic" />
</bean>

<bean id="oauthAccessDeniedHandler"
    class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />

<bean id="clientCredentialsTokenEndpointFilter"
    class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
    <property name="authenticationManager" ref="clientAuthenticationManager" />
</bean>

<bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased"
    xmlns="http://www.springframework.org/schema/beans">
    <constructor-arg>
        <list>
            <bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter" />
            <bean class="org.springframework.security.access.vote.RoleVoter" />
            <bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
        </list>
    </constructor-arg>
</bean>

<authentication-manager id="clientAuthenticationManager"
    xmlns="http://www.springframework.org/schema/security">
    <authentication-provider user-service-ref="clientDetailsUserService" />
</authentication-manager>

<bean id="clientDetailsUserService"
    class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
    <constructor-arg ref="clientDetails" />
</bean>


<!-- This defined token store, we have used inmemory tokenstore for now 
    but this can be changed to a user defined one -->
<bean id="tokenStore"
    class="org.springframework.security.oauth2.provider.token.InMemoryTokenStore" />


<!-- This is where we defined token based configurations, token validity 
    and other things -->
<bean id="tokenServices"
    class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
    <property name="tokenStore" ref="tokenStore" />
    <property name="supportRefreshToken" value="true" />
    <property name="accessTokenValiditySeconds" value="120" />
    <property name="clientDetailsService" ref="clientDetails" />
</bean>

<bean id="userApprovalHandler"
    class="org.springframework.security.oauth2.provider.approval.TokenServicesUserApprovalHandler">
    <property name="tokenServices" ref="tokenServices" />
</bean>

<oauth:authorization-server
    client-details-service-ref="clientDetails" token-services-ref="tokenServices"
    user-approval-handler-ref="userApprovalHandler">
    <oauth:authorization-code />
    <oauth:implicit />
    <oauth:refresh-token />
    <oauth:client-credentials />
    <oauth:password />
</oauth:authorization-server>


<oauth:resource-server id="resourceServerFilter"
    resource-id="test" token-services-ref="tokenServices" />

<oauth:client-details-service id="clientDetails">
    <!-- client -->
    <oauth:client client-id="restapp"
        authorized-grant-types="authorization_code,client_credentials"
        authorities="ROLE_APP" scope="read,write,trust" secret="secret" />

    <oauth:client client-id="restapp"
        authorized-grant-types="password,authorization_code,refresh_token,implicit"
        secret="restapp" authorities="ROLE_APP" />

</oauth:client-details-service>

<security:global-method-security
    pre-post-annotations="enabled" proxy-target-class="true">
    <!--you could also wire in the expression handler up at the layer of the 
        http filters. See https://jira.springsource.org/browse/SEC-1452 -->
    <security:expression-handler ref="oauthExpressionHandler" />
</security:global-method-security>

<oauth:expression-handler id="oauthExpressionHandler" />
<oauth:web-expression-handler id="oauthWebExpressionHandler" />



<!-- Spring security -->

<!-- <security:global-method-security
    secured-annotations="enabled" />
 -->


<security:http auto-config="false" authentication-manager-ref="authenticationManager" use-expressions="true" >
    <!-- Override default login and logout pages -->
    <security:form-login authentication-failure-handler-ref="failureClass" authentication-success-handler-ref="successClass"
        login-page="/login.xhtml" default-target-url="dashboard.xhtml" />
    <security:logout invalidate-session="true" logout-url="/j_spring_security_logout" success-handler-ref="LogoutAction" />  
    <security:session-management>
        <security:concurrency-control max-sessions="10" error-if-maximum-exceeded="true" />
    </security:session-management>  
    <security:intercept-url pattern="/jsf/**" access="isAuthenticated()" /> 
    <security:intercept-url pattern="/run**" access="isAuthenticated()" />  
    <security:intercept-url pattern="/login.xhtml" access="permitAll" />    
</security:http>

<bean id="successClass" class="com.car.SuccessAction"/>

<bean id="failureClass" class="com.car.FailureAction" >
    <property name="defaultFailureUrl" value="/?login_error=true"/>
</bean>
<bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder" />

<security:authentication-manager alias="authenticationManager">
    <security:authentication-provider user-service-ref="userDetailsService" >
        <security:password-encoder ref="passwordEncoder" hash="sha"/>
    </security:authentication-provider>
</security:authentication-manager>

Dependencies from pom.xml

Built using spring boot with the following relevant dependencies:

  • spring-boot-starter-web v2.2.4
  • azure-active-directory-spring-boot-starter v2.2.1
  • spring-security-oauth2-client v5.2.1
  • spring-security-oauth2-jose v5.2.1
  • spring-security-oauth2-resource-server v5.2.1
Похожее:  Настройка двухфакторной аутентификации при входе в Windows · Мультифактор

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *