io.gravitee.am.identityprovider.twitter.authentication.TwitterAuthenticationProvider Maven / Gradle / Ivy
The newest version!
/**
* Copyright (C) 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.gravitee.am.identityprovider.twitter.authentication;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.gravitee.am.common.exception.authentication.BadCredentialsException;
import io.gravitee.am.common.oauth2.Parameters;
import io.gravitee.am.common.oauth2.TokenTypeHint;
import io.gravitee.am.common.oidc.StandardClaims;
import io.gravitee.am.common.web.UriBuilder;
import io.gravitee.am.identityprovider.api.Authentication;
import io.gravitee.am.identityprovider.api.DefaultIdentityProviderGroupMapper;
import io.gravitee.am.identityprovider.api.DefaultUser;
import io.gravitee.am.identityprovider.api.IdentityProviderGroupMapper;
import io.gravitee.am.identityprovider.api.IdentityProviderMapper;
import io.gravitee.am.identityprovider.api.IdentityProviderRoleMapper;
import io.gravitee.am.identityprovider.api.User;
import io.gravitee.am.identityprovider.api.common.Request;
import io.gravitee.am.identityprovider.common.oauth2.authentication.AbstractSocialAuthenticationProvider;
import io.gravitee.am.identityprovider.twitter.TwitterIdentityProviderConfiguration;
import io.gravitee.am.identityprovider.twitter.authentication.spring.TwitterAuthenticationProviderConfiguration;
import io.gravitee.am.identityprovider.twitter.authentication.utils.OAuthCredentials;
import io.gravitee.common.http.HttpHeaders;
import io.gravitee.common.http.HttpMethod;
import io.gravitee.common.util.Maps;
import io.reactivex.rxjava3.core.Maybe;
import io.vertx.core.json.JsonObject;
import io.vertx.rxjava3.core.MultiMap;
import io.vertx.rxjava3.ext.web.client.WebClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Import;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_CALLBACK;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_CONSUMER_KEY;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_SIGNATURE_METHOD;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_SIGNATURE_METHOD_VALUE;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_TOKEN;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_TOKEN_SECRET;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_VERIFIER;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_VERSION;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.OAUTH_VERSION_VALUE;
import static io.gravitee.am.identityprovider.twitter.authentication.utils.SignerUtils.getAuthorizationHeader;
import static java.util.Collections.emptyMap;
/**
* @author Eric LELEU (eric.leleu at graviteesource.com)
* @author GraviteeSource Team
*/
@Slf4j
@Import(TwitterAuthenticationProviderConfiguration.class)
public class TwitterAuthenticationProvider extends AbstractSocialAuthenticationProvider {
private static final String FOLLOWERS = "followers";
private static final String FOLLOWERS_COUNT = "followers_count";
private static final String FRIENDS = "friends";
private static final String FRIENDS_COUNT = "friends_count";
private static final String DESCRIPTION = "description";
private static final String LOCATION = "location";
private static final String CREATED_AT = "create_at";
private static final String FULL_NAME = "full_name";
private static final String TWITTER_BASE_URL = "https://twitter.com/";
private static final String TWITTER_NAME = "name";
private static final String TWITTER_PROFILE_IMG = "profile_image_url_https";
private static final String TWITTER_SCREEN_NAME = "screen_name";
private static final String TWITTER_ID = "id_str";
private static final String TWITTER_EMAIL = "email";
private static final String TWITTER_LANG = "lang";
private static final String TWITTER_TIME_ZONE = "time_zone";
private static final String TWITTER_UPDATED_AT = "updated_at";
@Autowired
@Qualifier("twitterWebClient")
private WebClient client;
@Autowired
private IdentityProviderMapper mapper;
@Autowired
private IdentityProviderRoleMapper roleMapper;
@Autowired
private DefaultIdentityProviderGroupMapper groupMapper;
@Autowired
private TwitterIdentityProviderConfiguration configuration;
@Override
public TwitterIdentityProviderConfiguration getConfiguration() {
return this.configuration;
}
@Override
protected IdentityProviderMapper getIdentityProviderMapper() {
return this.mapper;
}
@Override
protected IdentityProviderRoleMapper getIdentityProviderRoleMapper() {
return this.roleMapper;
}
@Override
protected IdentityProviderGroupMapper getIdentityProviderGroupMapper() {
return this.groupMapper;
}
@Override
protected WebClient getClient() {
return this.client;
}
protected final Cache tokenMemory = CacheBuilder.newBuilder()
.expireAfterAccess(15, TimeUnit.MINUTES)
.expireAfterWrite(15, TimeUnit.MINUTES)
.build();
@Override
public Request signInUrl(String redirectUri, String state) {
throw new IllegalStateException("signInUrl isn't implemented for Twitter IdP");
}
@Override
public Maybe asyncSignInUrl(String redirectUri, String state) {
try {
if (StringUtils.hasText(state)) {
// Add state to redirect uri if specified. Note: Twitter is not oidc compliant and does not allow to specify a 'state' query parameter on its own authorization url.
final UriBuilder uriBuilder = UriBuilder.fromURIString(redirectUri).addParameter(Parameters.STATE, state);
redirectUri = uriBuilder.buildString();
}
Map parameters = Maps.builder()
.put(OAUTH_CALLBACK, redirectUri)
.put(OAUTH_CONSUMER_KEY, configuration.getClientId())
.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE)
.put(OAUTH_VERSION, OAUTH_VERSION_VALUE)
.build();
String authorization = getAuthorizationHeader("POST", configuration.getRequestTokenUrl(), emptyMap(), parameters, new OAuthCredentials(configuration));
return getClient().postAbs(getConfiguration().getRequestTokenUrl())
.putHeader(HttpHeaders.AUTHORIZATION, authorization)
.rxSend()
.toMaybe()
.map(httpResponse -> {
if (httpResponse.statusCode() != 200) {
throw new BadCredentialsException(httpResponse.statusMessage());
}
String body = httpResponse.bodyAsString();
String[] tokenResponse = body.split("&");
String token = null;
String tokenSecret = null;
String callbackState = null;
for (String responsePair : tokenResponse) {
String[] pair = responsePair.split("=");
if (pair.length > 1) {
if (OAUTH_TOKEN.equals(pair[0])) {
token = pair[1];
}
if (OAUTH_TOKEN_SECRET.equals(pair[0])) {
tokenSecret = pair[1];
}
if ("oauth_callback_confirmed".equals(pair[0])) {
callbackState = pair[1];
}
}
}
if ("true".equalsIgnoreCase(callbackState)) {
tokenMemory.put(token, tokenSecret); // preserve token & token secret for the next steps
UriBuilder builder = UriBuilder.fromHttpUrl(configuration.getUserAuthorizationUri());
builder.addParameter(OAUTH_TOKEN, token);
Request request = new Request();
request.setMethod(HttpMethod.GET);
request.setUri(builder.build().toString());
return request;
}
throw new BadCredentialsException("Token returned by Twitter mismatch");
});
} catch (BadCredentialsException e) {
log.error("An error occurs while building Sign In URL", e);
return Maybe.empty();
}
}
@Override
protected Maybe authenticate(Authentication authentication) {
final String oauthToken = authentication.getContext().request().parameters().getFirst(configuration.getCodeParameter());
final String tokenVerifier = authentication.getContext().request().parameters().getFirst(configuration.getTokenVerifier());
if (oauthToken == null || oauthToken.isEmpty() || tokenMemory.getIfPresent(oauthToken) == null) {
log.debug("OAuth Token is missing, skip authentication");
return Maybe.error(new BadCredentialsException("Missing OAuth Token"));
}
if (tokenVerifier == null || tokenVerifier.isEmpty()) {
log.debug("Token Verifier is missing, skip authentication");
return Maybe.error(new BadCredentialsException("Missing Token Verifier"));
}
Map parameters = Maps.builder()
.put(OAUTH_VERIFIER, tokenVerifier)
.build();
Map oauthParams = Maps.builder()
.put(OAUTH_CONSUMER_KEY, configuration.getClientId())
.put(OAUTH_TOKEN, oauthToken)
.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE)
.put(OAUTH_VERSION, OAUTH_VERSION_VALUE)
.build();
String authorization = getAuthorizationHeader("POST",
configuration.getAccessTokenUri(),
parameters, oauthParams,
new OAuthCredentials(configuration, oauthToken, tokenMemory.getIfPresent(oauthToken)));
tokenMemory.invalidate(oauthToken);
MultiMap form = MultiMap.caseInsensitiveMultiMap().set(OAUTH_VERIFIER, tokenVerifier);
return client.postAbs(configuration.getAccessTokenUri())
.putHeader(HttpHeaders.AUTHORIZATION, authorization)
.rxSendForm(form)
.toMaybe()
.flatMap(httpResponse -> {
if (httpResponse.statusCode() != 200) {
log.error("HTTP error {} is thrown while exchanging code. The response body is: {} ", httpResponse.statusCode(), httpResponse.bodyAsString());
return Maybe.error(new BadCredentialsException(httpResponse.bodyAsString()));
}
String[] tokenInfo = httpResponse.bodyAsString().split("&");
String token = "";
String secret = "";
for (String pairString : tokenInfo) {
String[] pair = pairString.split("=");
if (pair.length > 1) {
if (pair[0].equalsIgnoreCase(OAUTH_TOKEN)) {
token = pair[1];
}
if (pair[0].equalsIgnoreCase(OAUTH_TOKEN_SECRET)) {
secret = pair[1];
}
}
}
return Maybe.just(new Token(token, secret, TokenTypeHint.ACCESS_TOKEN));
});
}
@Override
protected Maybe profile(Token token, Authentication authentication) {
Map parameters = Maps.builder()
.put("include_email", "true")
.build();
Map oauthParams = Maps.builder()
.put(OAUTH_CONSUMER_KEY, configuration.getClientId())
.put(OAUTH_TOKEN, token.value())
.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE)
.put(OAUTH_VERSION, OAUTH_VERSION_VALUE)
.build();
String authorization = getAuthorizationHeader("GET",
configuration.getUserProfileUri(),
parameters, oauthParams,
new OAuthCredentials(configuration, token.value(), token.secret()));
return client.getAbs(configuration.getUserProfileUri() + "?include_email=true")
.putHeader(HttpHeaders.AUTHORIZATION, authorization)
//.rxSendForm(form)
.rxSend()
.toMaybe()
.flatMap(httpResponse -> {
if (httpResponse.statusCode() != 200) {
return Maybe.error(new BadCredentialsException(httpResponse.bodyAsString()));
}
JsonObject jsonObject = httpResponse.bodyAsJsonObject();
DefaultUser user = new DefaultUser(jsonObject.getString(TWITTER_SCREEN_NAME));
user.setId(jsonObject.getString(TWITTER_ID));
Map additionalInfos = new HashMap<>(applyUserMapping(authentication.getContext(), jsonObject.getMap()));
user.setAdditionalInformation(additionalInfos);
user.setRoles(applyRoleMapping(authentication.getContext(), jsonObject.getMap()));
user.setGroups(applyGroupMapping(authentication.getContext(), jsonObject.getMap()));
return Maybe.just(user);
});
}
@Override
protected Map defaultClaims(Map attributes) {
JsonObject jsonObject = JsonObject.mapFrom(attributes);
Map claims = new HashMap<>();
claims.put(StandardClaims.PROFILE, TWITTER_BASE_URL + jsonObject.getString(TWITTER_SCREEN_NAME));
claims.put(StandardClaims.PREFERRED_USERNAME, jsonObject.getString(TWITTER_SCREEN_NAME));
claims.put(StandardClaims.SUB, jsonObject.getString(TWITTER_ID));
final String img = jsonObject.getString(TWITTER_PROFILE_IMG);
if (img != null) {
claims.put(StandardClaims.PICTURE, img);
}
final String email = jsonObject.getString(TWITTER_EMAIL);
if (email != null) {
claims.put(StandardClaims.EMAIL, email);
}
final String zone = jsonObject.getString(TWITTER_TIME_ZONE);
if (zone != null) {
claims.put(StandardClaims.ZONEINFO, zone);
}
final String updateAt = jsonObject.getString(TWITTER_UPDATED_AT);
if (updateAt != null) {
claims.put(StandardClaims.UPDATED_AT, updateAt);
}
final String locale = jsonObject.getString(TWITTER_LANG);
if (locale != null) {
claims.put(StandardClaims.LOCALE, locale);
}
// custom Twitter claims
final String description = jsonObject.getString(DESCRIPTION);
if (description != null) {
claims.put(DESCRIPTION, description);
}
final String location = jsonObject.getString(LOCATION);
if (location != null) {
claims.put(LOCATION, location);
}
final String createdAt = jsonObject.getString(CREATED_AT);
if (createdAt != null) {
claims.put(CREATED_AT, createdAt);
}
final String fullName = jsonObject.getString(TWITTER_NAME);
if (fullName != null) {
claims.put(FULL_NAME, fullName);
}
claims.put(FOLLOWERS, jsonObject.getInteger(FOLLOWERS_COUNT, 0));
claims.put(FRIENDS, jsonObject.getInteger(FRIENDS_COUNT, 0));
return claims;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy