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.4azure-active-directory-spring-boot-starter
v2.2.1spring-security-oauth2-client
v5.2.1spring-security-oauth2-jose
v5.2.1spring-security-oauth2-resource-server
v5.2.1