getParams() {
+ return params;
+ }
+
+ public ParameterList sort() {
+ final ParameterList sorted = new ParameterList(params);
+ Collections.sort(sorted.getParams());
+ return sorted;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/model/Response.java b/scribejava-core/src/main/java/com/github/scribejava/core/model/Response.java
new file mode 100644
index 000000000..553c6cda5
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/model/Response.java
@@ -0,0 +1,154 @@
+package com.github.scribejava.core.model;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import com.github.scribejava.core.utils.StreamUtils;
+import java.io.Closeable;
+
+/**
+ * An HTTP response.
+ *
+ *
+ * This response may contain a non-null body stream of the HttpUrlConnection. If so, this body must be closed to avoid
+ * leaking resources. Use either {@link #getBody()} or {@link #close()} to close the body.
+ */
+public class Response implements Closeable {
+
+ private final int code;
+ private final String message;
+ private final Map headers;
+ private String body;
+ private InputStream stream;
+ private Closeable[] closeables;
+ private boolean closed;
+
+ private Response(int code, String message, Map headers) {
+ this.code = code;
+ this.message = message;
+ this.headers = headers;
+ }
+
+ public Response(int code, String message, Map headers, InputStream stream,
+ Closeable... closeables) {
+ this(code, message, headers);
+ this.stream = stream;
+ this.closeables = closeables;
+ }
+
+ public Response(int code, String message, Map headers, String body) {
+ this(code, message, headers);
+ this.body = body;
+ }
+
+ private String parseBodyContents() throws IOException {
+ if (stream == null) {
+ return null;
+ }
+ if ("gzip".equals(getHeader("Content-Encoding"))) {
+ body = StreamUtils.getGzipStreamContents(stream);
+ } else {
+ body = StreamUtils.getStreamContents(stream);
+ }
+ return body;
+ }
+
+ public boolean isSuccessful() {
+ return code >= 200 && code < 400;
+ }
+
+ /**
+ * Returns the response body as a string, closing the stream that backs it. Idempotent.
+ *
+ * @return body as string
+ * @throws IOException IO Exception
+ */
+ public String getBody() throws IOException {
+ return body == null ? parseBodyContents() : body;
+ }
+
+ /**
+ * Obtains the meaningful stream of the HttpUrlConnection, either inputStream or errorInputStream, depending on the
+ * status code
+ *
+ * @return input stream / error stream
+ */
+ public InputStream getStream() {
+ return stream;
+ }
+
+ /**
+ * Obtains the HTTP status code
+ *
+ * @return the status code
+ */
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * Obtains the HTTP status message. Returns null if the message can not be discerned from the response
+ * (not valid HTTP)
+ *
+ * @return the status message
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Obtains a {@link Map} containing the HTTP Response Headers
+ *
+ * @return headers
+ */
+ public Map getHeaders() {
+ return headers;
+ }
+
+ /**
+ * Obtains a single HTTP Header value, or null if undefined
+ *
+ * @param name the header name.
+ *
+ * @return header value or null.
+ */
+ public String getHeader(String name) {
+ return headers.get(name);
+ }
+
+ @Override
+ public String toString() {
+ return "Response{"
+ + "code=" + code
+ + ", message='" + message + '\''
+ + ", body='" + body + '\''
+ + ", headers=" + headers
+ + '}';
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (closed) {
+ return;
+ }
+ IOException ioException = null;
+ if (closeables != null) {
+ for (Closeable closeable : closeables) {
+ if (closeable == null) {
+ continue;
+ }
+ try {
+ closeable.close();
+ } catch (IOException ioE) {
+ if (ioException != null) {
+ ioException = ioE;
+ }
+ }
+ }
+ }
+ if (ioException != null) {
+ throw ioException;
+ }
+ closed = true;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/model/Token.java b/scribejava-core/src/main/java/com/github/scribejava/core/model/Token.java
new file mode 100644
index 000000000..ede417c03
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/model/Token.java
@@ -0,0 +1,39 @@
+package com.github.scribejava.core.model;
+
+import java.io.Serializable;
+
+/**
+ * Represents an abstract OAuth (1 and 2) token (either request or access token)
+ */
+public abstract class Token implements Serializable {
+
+ private static final long serialVersionUID = -8409640649946468092L;
+
+ private final String rawResponse;
+
+ protected Token(String rawResponse) {
+ this.rawResponse = rawResponse;
+ }
+
+ public String getRawResponse() {
+ if (rawResponse == null) {
+ throw new IllegalStateException(
+ "This token object was not constructed by ScribeJava and does not have a rawResponse");
+ }
+ return rawResponse;
+ }
+
+ public String getParameter(String parameter) {
+ String value = null;
+ for (String str : rawResponse.split("&")) {
+ if (str.startsWith(parameter + '=')) {
+ final String[] part = str.split("=");
+ if (part.length > 1) {
+ value = part[1].trim();
+ }
+ break;
+ }
+ }
+ return value;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/model/Verb.java b/scribejava-core/src/main/java/com/github/scribejava/core/model/Verb.java
new file mode 100644
index 000000000..8df5fd3f3
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/model/Verb.java
@@ -0,0 +1,32 @@
+package com.github.scribejava.core.model;
+
+/**
+ * An enumeration containing the most common HTTP Verbs.
+ */
+public enum Verb {
+
+ GET(false), POST(true), PUT(true), DELETE(false, true), HEAD(false), OPTIONS(false), TRACE(false), PATCH(true);
+
+ private final boolean requiresBody;
+ private final boolean permitBody;
+
+ Verb(boolean requiresBody) {
+ this(requiresBody, requiresBody);
+ }
+
+ Verb(boolean requiresBody, boolean permitBody) {
+ if (requiresBody && !permitBody) {
+ throw new IllegalArgumentException();
+ }
+ this.requiresBody = requiresBody;
+ this.permitBody = permitBody;
+ }
+
+ public boolean isRequiresBody() {
+ return requiresBody;
+ }
+
+ public boolean isPermitBody() {
+ return permitBody;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AccessTokenRequestParams.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AccessTokenRequestParams.java
new file mode 100644
index 000000000..d45377a56
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AccessTokenRequestParams.java
@@ -0,0 +1,79 @@
+package com.github.scribejava.core.oauth;
+
+import com.github.scribejava.core.builder.ScopeBuilder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * not thread safe
+ */
+public class AccessTokenRequestParams {
+
+ private final String code;
+ private String pkceCodeVerifier;
+ private String scope;
+ private Map extraParameters;
+
+ public AccessTokenRequestParams(String code) {
+ this.code = code;
+ }
+
+ public static AccessTokenRequestParams create(String code) {
+ return new AccessTokenRequestParams(code);
+ }
+
+ public AccessTokenRequestParams pkceCodeVerifier(String pkceCodeVerifier) {
+ this.pkceCodeVerifier = pkceCodeVerifier;
+ return this;
+ }
+
+ public AccessTokenRequestParams scope(String scope) {
+ this.scope = scope;
+ return this;
+ }
+
+ public AccessTokenRequestParams scope(ScopeBuilder scope) {
+ this.scope = scope.build();
+ return this;
+ }
+
+ public AccessTokenRequestParams addExtraParameters(Map extraParameters) {
+ if (extraParameters == null || extraParameters.isEmpty()) {
+ return this;
+ }
+ if (this.extraParameters == null) {
+ extraParameters = new HashMap<>();
+ }
+ this.extraParameters.putAll(extraParameters);
+ return this;
+ }
+
+ public AccessTokenRequestParams addExtraParameter(String name, String value) {
+ if (this.extraParameters == null) {
+ extraParameters = new HashMap<>();
+ }
+ this.extraParameters.put(name, value);
+ return this;
+ }
+
+ public AccessTokenRequestParams setExtraParameters(Map extraParameters) {
+ this.extraParameters = extraParameters;
+ return this;
+ }
+
+ public Map getExtraParameters() {
+ return extraParameters;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getPkceCodeVerifier() {
+ return pkceCodeVerifier;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AuthorizationUrlBuilder.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AuthorizationUrlBuilder.java
new file mode 100644
index 000000000..fd02f60dd
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AuthorizationUrlBuilder.java
@@ -0,0 +1,61 @@
+package com.github.scribejava.core.oauth;
+
+import com.github.scribejava.core.pkce.PKCE;
+import com.github.scribejava.core.pkce.PKCEService;
+import java.util.HashMap;
+import java.util.Map;
+
+public class AuthorizationUrlBuilder {
+
+ private final OAuth20Service oauth20Service;
+
+ private String state;
+ private Map additionalParams;
+ private PKCE pkce;
+ private String scope;
+
+ public AuthorizationUrlBuilder(OAuth20Service oauth20Service) {
+ this.oauth20Service = oauth20Service;
+ }
+
+ public AuthorizationUrlBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ public AuthorizationUrlBuilder additionalParams(Map additionalParams) {
+ this.additionalParams = additionalParams;
+ return this;
+ }
+
+ public AuthorizationUrlBuilder pkce(PKCE pkce) {
+ this.pkce = pkce;
+ return this;
+ }
+
+ public AuthorizationUrlBuilder initPKCE() {
+ this.pkce = PKCEService.defaultInstance().generatePKCE();
+ return this;
+ }
+
+ public AuthorizationUrlBuilder scope(String scope) {
+ this.scope = scope;
+ return this;
+ }
+
+ public PKCE getPkce() {
+ return pkce;
+ }
+
+ public String build() {
+ final Map params;
+ if (pkce == null) {
+ params = additionalParams;
+ } else {
+ params = additionalParams == null ? new HashMap() : new HashMap<>(additionalParams);
+ params.putAll(pkce.getAuthorizationUrlParams());
+ }
+ return oauth20Service.getApi().getAuthorizationUrl(oauth20Service.getResponseType(), oauth20Service.getApiKey(),
+ oauth20Service.getCallback(), scope == null ? oauth20Service.getDefaultScope() : scope, state, params);
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth10aService.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth10aService.java
new file mode 100644
index 000000000..f48308b2e
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth10aService.java
@@ -0,0 +1,220 @@
+package com.github.scribejava.core.oauth;
+
+import java.io.IOException;
+import java.util.concurrent.Future;
+import com.github.scribejava.core.builder.api.DefaultApi10a;
+import com.github.scribejava.core.builder.api.OAuth1SignatureType;
+import com.github.scribejava.core.httpclient.HttpClient;
+import com.github.scribejava.core.httpclient.HttpClientConfig;
+import com.github.scribejava.core.model.OAuth1AccessToken;
+import com.github.scribejava.core.model.OAuth1RequestToken;
+import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Response;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * OAuth 1.0a implementation of {@link OAuthService}
+ */
+public class OAuth10aService extends OAuthService {
+
+ private static final String VERSION = "1.0";
+ private final DefaultApi10a api;
+ private final String scope;
+
+ public OAuth10aService(DefaultApi10a api, String apiKey, String apiSecret, String callback, String scope,
+ OutputStream debugStream, String userAgent, HttpClientConfig httpClientConfig, HttpClient httpClient) {
+ super(apiKey, apiSecret, callback, debugStream, userAgent, httpClientConfig, httpClient);
+ this.api = api;
+ this.scope = scope;
+ }
+
+ public OAuth1RequestToken getRequestToken() throws IOException, InterruptedException, ExecutionException {
+ if (isDebug()) {
+ log("obtaining request token from %s", api.getRequestTokenEndpoint());
+ }
+ final OAuthRequest request = prepareRequestTokenRequest();
+
+ log("sending request...");
+ try ( Response response = execute(request)) {
+ if (isDebug()) {
+ final String body = response.getBody();
+ log("response status code: %s", response.getCode());
+ log("response body: %s", body);
+ }
+ return api.getRequestTokenExtractor().extract(response);
+ }
+ }
+
+ public Future getRequestTokenAsync() {
+ return getRequestTokenAsync(null);
+ }
+
+ public Future getRequestTokenAsync(OAuthAsyncRequestCallback callback) {
+ if (isDebug()) {
+ log("async obtaining request token from %s", api.getRequestTokenEndpoint());
+ }
+ final OAuthRequest request = prepareRequestTokenRequest();
+ return execute(request, callback, new OAuthRequest.ResponseConverter() {
+ @Override
+ public OAuth1RequestToken convert(Response response) throws IOException {
+ final OAuth1RequestToken token = getApi().getRequestTokenExtractor().extract(response);
+ response.close();
+ return token;
+ }
+ });
+ }
+
+ protected OAuthRequest prepareRequestTokenRequest() {
+ final OAuthRequest request = new OAuthRequest(api.getRequestTokenVerb(), api.getRequestTokenEndpoint());
+ String callback = getCallback();
+ if (callback == null) {
+ callback = OAuthConstants.OOB;
+ }
+ if (isDebug()) {
+ log("setting oauth_callback to %s", callback);
+ }
+ request.addOAuthParameter(OAuthConstants.CALLBACK, callback);
+ addOAuthParams(request, "");
+ appendSignature(request);
+ return request;
+ }
+
+ protected void addOAuthParams(OAuthRequest request, String tokenSecret) {
+ request.addOAuthParameter(OAuthConstants.TIMESTAMP, api.getTimestampService().getTimestampInSeconds());
+ request.addOAuthParameter(OAuthConstants.NONCE, api.getTimestampService().getNonce());
+ request.addOAuthParameter(OAuthConstants.CONSUMER_KEY, getApiKey());
+ request.addOAuthParameter(OAuthConstants.SIGN_METHOD, api.getSignatureService().getSignatureMethod());
+ request.addOAuthParameter(OAuthConstants.VERSION, getVersion());
+ if (scope != null) {
+ request.addOAuthParameter(OAuthConstants.SCOPE, scope);
+ }
+ request.addOAuthParameter(OAuthConstants.SIGNATURE, getSignature(request, tokenSecret));
+
+ if (isDebug()) {
+ log("appended additional OAuth parameters: %s", request.getOauthParameters());
+ }
+ }
+
+ public OAuth1AccessToken getAccessToken(OAuth1RequestToken requestToken, String oauthVerifier)
+ throws IOException, InterruptedException, ExecutionException {
+ if (isDebug()) {
+ log("obtaining access token from %s", api.getAccessTokenEndpoint());
+ }
+ final OAuthRequest request = prepareAccessTokenRequest(requestToken, oauthVerifier);
+ try ( Response response = execute(request)) {
+ return api.getAccessTokenExtractor().extract(response);
+ }
+ }
+
+ public Future getAccessTokenAsync(OAuth1RequestToken requestToken, String oauthVerifier) {
+ return getAccessTokenAsync(requestToken, oauthVerifier, null);
+ }
+
+ /**
+ * Start the request to retrieve the access token. The optionally provided callback will be called with the Token
+ * when it is available.
+ *
+ * @param requestToken request token (obtained previously or null)
+ * @param oauthVerifier oauth_verifier
+ * @param callback optional callback
+ * @return Future
+ */
+ public Future getAccessTokenAsync(OAuth1RequestToken requestToken, String oauthVerifier,
+ OAuthAsyncRequestCallback callback) {
+ if (isDebug()) {
+ log("async obtaining access token from %s", api.getAccessTokenEndpoint());
+ }
+ final OAuthRequest request = prepareAccessTokenRequest(requestToken, oauthVerifier);
+ return execute(request, callback, new OAuthRequest.ResponseConverter() {
+ @Override
+ public OAuth1AccessToken convert(Response response) throws IOException {
+ final OAuth1AccessToken token = getApi().getAccessTokenExtractor().extract(response);
+ response.close();
+ return token;
+ }
+ });
+ }
+
+ protected OAuthRequest prepareAccessTokenRequest(OAuth1RequestToken requestToken, String oauthVerifier) {
+ final OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
+ request.addOAuthParameter(OAuthConstants.TOKEN, requestToken.getToken());
+ request.addOAuthParameter(OAuthConstants.VERIFIER, oauthVerifier);
+ if (isDebug()) {
+ log("setting token to: %s and verifier to: %s", requestToken, oauthVerifier);
+ }
+ addOAuthParams(request, requestToken.getTokenSecret());
+ appendSignature(request);
+ return request;
+ }
+
+ public void signRequest(OAuth1AccessToken token, OAuthRequest request) {
+ if (isDebug()) {
+ log("signing request: %s", request.getCompleteUrl());
+ }
+
+ if (!token.isEmpty() || api.isEmptyOAuthTokenParamIsRequired()) {
+ request.addOAuthParameter(OAuthConstants.TOKEN, token.getToken());
+ }
+ if (isDebug()) {
+ log("setting token to: %s", token);
+ }
+ addOAuthParams(request, token.getTokenSecret());
+ appendSignature(request);
+ }
+
+ @Override
+ public String getVersion() {
+ return VERSION;
+ }
+
+ /**
+ * Returns the URL where you should redirect your users to authenticate your application.
+ *
+ * @param requestToken the request token you need to authorize
+ * @return the URL where you should redirect your users
+ */
+ public String getAuthorizationUrl(OAuth1RequestToken requestToken) {
+ return api.getAuthorizationUrl(requestToken);
+ }
+
+ private String getSignature(OAuthRequest request, String tokenSecret) {
+ log("generating signature...");
+ final String baseString = api.getBaseStringExtractor().extract(request);
+ final String signature = api.getSignatureService().getSignature(baseString, getApiSecret(), tokenSecret);
+
+ if (isDebug()) {
+ log("base string is: %s", baseString);
+ log("signature is: %s", signature);
+ }
+ return signature;
+ }
+
+ protected void appendSignature(OAuthRequest request) {
+ final OAuth1SignatureType signatureType = api.getSignatureType();
+ switch (signatureType) {
+ case HEADER:
+ log("using Http Header signature");
+
+ final String oauthHeader = api.getHeaderExtractor().extract(request);
+ request.addHeader(OAuthConstants.HEADER, oauthHeader);
+ break;
+ case QUERY_STRING:
+ log("using Querystring signature");
+
+ for (Map.Entry oauthParameter : request.getOauthParameters().entrySet()) {
+ request.addQuerystringParameter(oauthParameter.getKey(), oauthParameter.getValue());
+ }
+ break;
+ default:
+ throw new IllegalStateException("Unknown new Signature Type '" + signatureType + "'.");
+ }
+ }
+
+ public DefaultApi10a getApi() {
+ return api;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth20Service.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth20Service.java
new file mode 100644
index 000000000..82052e351
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth20Service.java
@@ -0,0 +1,676 @@
+package com.github.scribejava.core.oauth;
+
+import com.github.scribejava.core.builder.api.DefaultApi20;
+import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
+import com.github.scribejava.core.httpclient.HttpClient;
+import com.github.scribejava.core.httpclient.HttpClientConfig;
+import com.github.scribejava.core.model.DeviceAuthorization;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.model.OAuth2AccessTokenErrorResponse;
+import com.github.scribejava.core.model.OAuth2Authorization;
+import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Response;
+import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.oauth2.OAuth2Error;
+import com.github.scribejava.core.pkce.PKCE;
+import com.github.scribejava.core.revoke.TokenTypeHint;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+public class OAuth20Service extends OAuthService {
+
+ private static final String VERSION = "2.0";
+ private final DefaultApi20 api;
+ private final String responseType;
+ private final String defaultScope;
+
+ public OAuth20Service(DefaultApi20 api, String apiKey, String apiSecret, String callback, String defaultScope,
+ String responseType, OutputStream debugStream, String userAgent, HttpClientConfig httpClientConfig,
+ HttpClient httpClient) {
+ super(apiKey, apiSecret, callback, debugStream, userAgent, httpClientConfig, httpClient);
+ this.responseType = responseType;
+ this.api = api;
+ this.defaultScope = defaultScope;
+ }
+
+ // ===== common OAuth methods =====
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getVersion() {
+ return VERSION;
+ }
+
+ public void signRequest(String accessToken, OAuthRequest request) {
+ api.getBearerSignature().signRequest(accessToken, request);
+ }
+
+ public void signRequest(OAuth2AccessToken accessToken, OAuthRequest request) {
+ signRequest(accessToken == null ? null : accessToken.getAccessToken(), request);
+ }
+
+ /**
+ * Returns the URL where you should redirect your users to authenticate your application.
+ *
+ * @return the URL where you should redirect your users
+ */
+ public String getAuthorizationUrl() {
+ return createAuthorizationUrlBuilder().build();
+ }
+
+ public String getAuthorizationUrl(String state) {
+ return createAuthorizationUrlBuilder()
+ .state(state)
+ .build();
+ }
+
+ /**
+ * Returns the URL where you should redirect your users to authenticate your application.
+ *
+ * @param additionalParams any additional GET params to add to the URL
+ * @return the URL where you should redirect your users
+ */
+ public String getAuthorizationUrl(Map additionalParams) {
+ return createAuthorizationUrlBuilder()
+ .additionalParams(additionalParams)
+ .build();
+ }
+
+ public String getAuthorizationUrl(PKCE pkce) {
+ return createAuthorizationUrlBuilder()
+ .pkce(pkce)
+ .build();
+ }
+
+ public AuthorizationUrlBuilder createAuthorizationUrlBuilder() {
+ return new AuthorizationUrlBuilder(this);
+ }
+
+ public DefaultApi20 getApi() {
+ return api;
+ }
+
+ public OAuth2Authorization extractAuthorization(String redirectLocation) {
+ final OAuth2Authorization authorization = new OAuth2Authorization();
+ int end = redirectLocation.indexOf('#');
+ if (end == -1) {
+ end = redirectLocation.length();
+ }
+ for (String param : redirectLocation.substring(redirectLocation.indexOf('?') + 1, end).split("&")) {
+ final String[] keyValue = param.split("=");
+ if (keyValue.length == 2) {
+ try {
+ switch (keyValue[0]) {
+ case "code":
+ authorization.setCode(URLDecoder.decode(keyValue[1], "UTF-8"));
+ break;
+ case "state":
+ authorization.setState(URLDecoder.decode(keyValue[1], "UTF-8"));
+ break;
+ default: //just ignore any other param;
+ }
+ } catch (UnsupportedEncodingException ueE) {
+ throw new IllegalStateException("jvm without UTF-8, really?", ueE);
+ }
+ }
+ }
+ return authorization;
+ }
+
+ public String getResponseType() {
+ return responseType;
+ }
+
+ public String getDefaultScope() {
+ return defaultScope;
+ }
+
+ protected void logRequestWithParams(String requestDescription, OAuthRequest request) {
+ if (isDebug()) {
+ log("created " + requestDescription + " request with body params [%s], query string params [%s]",
+ request.getBodyParams().asFormUrlEncodedString(),
+ request.getQueryStringParams().asFormUrlEncodedString());
+ }
+ }
+
+ // ===== common AccessToken request methods =====
+ //protected to facilitate mocking
+ protected OAuth2AccessToken sendAccessTokenRequestSync(OAuthRequest request)
+ throws IOException, InterruptedException, ExecutionException {
+ if (isDebug()) {
+ log("send request for access token synchronously to %s", request.getCompleteUrl());
+ }
+ try (Response response = execute(request)) {
+ if (isDebug()) {
+ log("response status code: %s", response.getCode());
+ log("response body: %s", response.getBody());
+ }
+
+ return api.getAccessTokenExtractor().extract(response);
+ }
+ }
+
+ //protected to facilitate mocking
+ protected Future sendAccessTokenRequestAsync(OAuthRequest request) {
+ return sendAccessTokenRequestAsync(request, null);
+ }
+
+ //protected to facilitate mocking
+ protected Future sendAccessTokenRequestAsync(OAuthRequest request,
+ OAuthAsyncRequestCallback callback) {
+ if (isDebug()) {
+ log("send request for access token asynchronously to %s", request.getCompleteUrl());
+ }
+
+ return execute(request, callback, new OAuthRequest.ResponseConverter() {
+ @Override
+ public OAuth2AccessToken convert(Response response) throws IOException {
+ log("received response for access token");
+ if (isDebug()) {
+ log("response status code: %s", response.getCode());
+ log("response body: %s", response.getBody());
+ }
+ final OAuth2AccessToken token = api.getAccessTokenExtractor().extract(response);
+ response.close();
+ return token;
+ }
+ });
+ }
+
+ // ===== get AccessToken authorisation code flow methods =====
+ protected OAuthRequest createAccessTokenRequest(AccessTokenRequestParams params) {
+ final OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
+
+ api.getClientAuthentication().addClientAuthentication(request, getApiKey(), getApiSecret());
+
+ request.addParameter(OAuthConstants.CODE, params.getCode());
+ final String callback = getCallback();
+ if (callback != null) {
+ request.addParameter(OAuthConstants.REDIRECT_URI, callback);
+ }
+ final String scope = params.getScope();
+ if (scope != null) {
+ request.addParameter(OAuthConstants.SCOPE, scope);
+ } else if (defaultScope != null) {
+ request.addParameter(OAuthConstants.SCOPE, defaultScope);
+ }
+ request.addParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE);
+
+ final String pkceCodeVerifier = params.getPkceCodeVerifier();
+ if (pkceCodeVerifier != null) {
+ request.addParameter(PKCE.PKCE_CODE_VERIFIER_PARAM, pkceCodeVerifier);
+ }
+
+ final Map extraParameters = params.getExtraParameters();
+ if (extraParameters != null && !extraParameters.isEmpty()) {
+ for (Map.Entry extraParameter : extraParameters.entrySet()) {
+ request.addParameter(extraParameter.getKey(), extraParameter.getValue());
+ }
+ }
+
+ logRequestWithParams("access token", request);
+ return request;
+ }
+
+ public Future getAccessTokenAsync(String code) {
+ return getAccessToken(AccessTokenRequestParams.create(code), null);
+ }
+
+ public Future getAccessTokenAsync(AccessTokenRequestParams params) {
+ return getAccessToken(params, null);
+ }
+
+ public OAuth2AccessToken getAccessToken(String code) throws IOException, InterruptedException, ExecutionException {
+ return getAccessToken(AccessTokenRequestParams.create(code));
+ }
+
+ public OAuth2AccessToken getAccessToken(AccessTokenRequestParams params)
+ throws IOException, InterruptedException, ExecutionException {
+ return sendAccessTokenRequestSync(createAccessTokenRequest(params));
+ }
+
+ /**
+ * Start the request to retrieve the access token. The optionally provided callback will be called with the Token
+ * when it is available.
+ *
+ * @param params params
+ * @param callback optional callback
+ * @return Future
+ */
+ public Future getAccessToken(AccessTokenRequestParams params,
+ OAuthAsyncRequestCallback callback) {
+ return sendAccessTokenRequestAsync(createAccessTokenRequest(params), callback);
+ }
+
+ public Future getAccessToken(String code,
+ OAuthAsyncRequestCallback callback) {
+ return getAccessToken(AccessTokenRequestParams.create(code), callback);
+ }
+
+ // ===== refresh AccessToken methods =====
+ protected OAuthRequest createRefreshTokenRequest(String refreshToken, String scope) {
+ if (refreshToken == null || refreshToken.isEmpty()) {
+ throw new IllegalArgumentException("The refreshToken cannot be null or empty");
+ }
+ final OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getRefreshTokenEndpoint());
+
+ api.getClientAuthentication().addClientAuthentication(request, getApiKey(), getApiSecret());
+
+ if (scope != null) {
+ request.addParameter(OAuthConstants.SCOPE, scope);
+ } else if (defaultScope != null) {
+ request.addParameter(OAuthConstants.SCOPE, defaultScope);
+ }
+
+ request.addParameter(OAuthConstants.REFRESH_TOKEN, refreshToken);
+ request.addParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.REFRESH_TOKEN);
+
+ logRequestWithParams("refresh token", request);
+
+ return request;
+ }
+
+ public Future refreshAccessTokenAsync(String refreshToken) {
+ return refreshAccessToken(refreshToken, (OAuthAsyncRequestCallback) null);
+ }
+
+ public Future refreshAccessTokenAsync(String refreshToken, String scope) {
+ return refreshAccessToken(refreshToken, scope, null);
+ }
+
+ public OAuth2AccessToken refreshAccessToken(String refreshToken)
+ throws IOException, InterruptedException, ExecutionException {
+ return refreshAccessToken(refreshToken, (String) null);
+ }
+
+ public OAuth2AccessToken refreshAccessToken(String refreshToken, String scope)
+ throws IOException, InterruptedException, ExecutionException {
+ final OAuthRequest request = createRefreshTokenRequest(refreshToken, scope);
+
+ return sendAccessTokenRequestSync(request);
+ }
+
+ public Future refreshAccessToken(String refreshToken,
+ OAuthAsyncRequestCallback callback) {
+ final OAuthRequest request = createRefreshTokenRequest(refreshToken, null);
+
+ return sendAccessTokenRequestAsync(request, callback);
+ }
+
+ public Future refreshAccessToken(String refreshToken, String scope,
+ OAuthAsyncRequestCallback callback) {
+ final OAuthRequest request = createRefreshTokenRequest(refreshToken, scope);
+
+ return sendAccessTokenRequestAsync(request, callback);
+ }
+
+ // ===== get AccessToken password grant flow methods =====
+ protected OAuthRequest createAccessTokenPasswordGrantRequest(String username, String password, String scope) {
+ final OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
+ request.addParameter(OAuthConstants.USERNAME, username);
+ request.addParameter(OAuthConstants.PASSWORD, password);
+
+ if (scope != null) {
+ request.addParameter(OAuthConstants.SCOPE, scope);
+ } else if (defaultScope != null) {
+ request.addParameter(OAuthConstants.SCOPE, defaultScope);
+ }
+
+ request.addParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.PASSWORD);
+
+ api.getClientAuthentication().addClientAuthentication(request, getApiKey(), getApiSecret());
+
+ logRequestWithParams("access token password grant", request);
+
+ return request;
+ }
+
+ public OAuth2AccessToken getAccessTokenPasswordGrant(String username, String password)
+ throws IOException, InterruptedException, ExecutionException {
+ final OAuthRequest request = createAccessTokenPasswordGrantRequest(username, password, null);
+
+ return sendAccessTokenRequestSync(request);
+ }
+
+ public OAuth2AccessToken getAccessTokenPasswordGrant(String username, String password, String scope)
+ throws IOException, InterruptedException, ExecutionException {
+ final OAuthRequest request = createAccessTokenPasswordGrantRequest(username, password, scope);
+
+ return sendAccessTokenRequestSync(request);
+ }
+
+ public Future getAccessTokenPasswordGrantAsync(String username, String password) {
+ return getAccessTokenPasswordGrantAsync(username, password,
+ (OAuthAsyncRequestCallback) null);
+ }
+
+ public Future getAccessTokenPasswordGrantAsync(String username, String password, String scope) {
+ return getAccessTokenPasswordGrantAsync(username, password, scope, null);
+ }
+
+ /**
+ * Request Access Token Password Grant async version
+ *
+ * @param username User name
+ * @param password User password
+ * @param callback Optional callback
+ * @return Future
+ */
+ public Future getAccessTokenPasswordGrantAsync(String username, String password,
+ OAuthAsyncRequestCallback callback) {
+ final OAuthRequest request = createAccessTokenPasswordGrantRequest(username, password, null);
+
+ return sendAccessTokenRequestAsync(request, callback);
+ }
+
+ public Future getAccessTokenPasswordGrantAsync(String username, String password, String scope,
+ OAuthAsyncRequestCallback callback) {
+ final OAuthRequest request = createAccessTokenPasswordGrantRequest(username, password, scope);
+
+ return sendAccessTokenRequestAsync(request, callback);
+ }
+
+ // ===== get AccessToken client credentials flow methods =====
+ protected OAuthRequest createAccessTokenClientCredentialsGrantRequest(String scope) {
+ final OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
+
+ api.getClientAuthentication().addClientAuthentication(request, getApiKey(), getApiSecret());
+
+ if (scope != null) {
+ request.addParameter(OAuthConstants.SCOPE, scope);
+ } else if (defaultScope != null) {
+ request.addParameter(OAuthConstants.SCOPE, defaultScope);
+ }
+ request.addParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.CLIENT_CREDENTIALS);
+
+ logRequestWithParams("access token client credentials grant", request);
+
+ return request;
+ }
+
+ public Future getAccessTokenClientCredentialsGrantAsync() {
+ return getAccessTokenClientCredentialsGrant((OAuthAsyncRequestCallback) null);
+ }
+
+ public Future getAccessTokenClientCredentialsGrantAsync(String scope) {
+ return getAccessTokenClientCredentialsGrant(scope, null);
+ }
+
+ public OAuth2AccessToken getAccessTokenClientCredentialsGrant()
+ throws IOException, InterruptedException, ExecutionException {
+ final OAuthRequest request = createAccessTokenClientCredentialsGrantRequest(null);
+
+ return sendAccessTokenRequestSync(request);
+ }
+
+ public OAuth2AccessToken getAccessTokenClientCredentialsGrant(String scope)
+ throws IOException, InterruptedException, ExecutionException {
+ final OAuthRequest request = createAccessTokenClientCredentialsGrantRequest(scope);
+
+ return sendAccessTokenRequestSync(request);
+ }
+
+ /**
+ * Start the request to retrieve the access token using client-credentials grant. The optionally provided callback
+ * will be called with the Token when it is available.
+ *
+ * @param callback optional callback
+ * @return Future
+ */
+ public Future getAccessTokenClientCredentialsGrant(
+ OAuthAsyncRequestCallback callback) {
+ final OAuthRequest request = createAccessTokenClientCredentialsGrantRequest(null);
+
+ return sendAccessTokenRequestAsync(request, callback);
+ }
+
+ public Future getAccessTokenClientCredentialsGrant(String scope,
+ OAuthAsyncRequestCallback callback) {
+ final OAuthRequest request = createAccessTokenClientCredentialsGrantRequest(scope);
+
+ return sendAccessTokenRequestAsync(request, callback);
+ }
+
+ // ===== revoke AccessToken methods =====
+ protected OAuthRequest createRevokeTokenRequest(String tokenToRevoke, TokenTypeHint tokenTypeHint) {
+ final OAuthRequest request = new OAuthRequest(Verb.POST, api.getRevokeTokenEndpoint());
+
+ api.getClientAuthentication().addClientAuthentication(request, getApiKey(), getApiSecret());
+
+ request.addParameter("token", tokenToRevoke);
+ if (tokenTypeHint != null) {
+ request.addParameter("token_type_hint", tokenTypeHint.getValue());
+ }
+
+ logRequestWithParams("revoke token", request);
+
+ return request;
+ }
+
+ public Future revokeTokenAsync(String tokenToRevoke) {
+ return revokeTokenAsync(tokenToRevoke, null);
+ }
+
+ public Future revokeTokenAsync(String tokenToRevoke, TokenTypeHint tokenTypeHint) {
+ return revokeToken(tokenToRevoke, null, tokenTypeHint);
+ }
+
+ public void revokeToken(String tokenToRevoke) throws IOException, InterruptedException, ExecutionException {
+ revokeToken(tokenToRevoke, (TokenTypeHint) null);
+ }
+
+ public void revokeToken(String tokenToRevoke, TokenTypeHint tokenTypeHint)
+ throws IOException, InterruptedException, ExecutionException {
+ final OAuthRequest request = createRevokeTokenRequest(tokenToRevoke, tokenTypeHint);
+
+ try (Response response = execute(request)) {
+ checkForErrorRevokeToken(response);
+ }
+ }
+
+ public Future revokeToken(String tokenToRevoke, OAuthAsyncRequestCallback callback) {
+ return revokeToken(tokenToRevoke, callback, null);
+ }
+
+ public Future revokeToken(String tokenToRevoke, OAuthAsyncRequestCallback callback,
+ TokenTypeHint tokenTypeHint) {
+ final OAuthRequest request = createRevokeTokenRequest(tokenToRevoke, tokenTypeHint);
+
+ return execute(request, callback, new OAuthRequest.ResponseConverter() {
+ @Override
+ public Void convert(Response response) throws IOException {
+ checkForErrorRevokeToken(response);
+ response.close();
+ return null;
+ }
+ });
+ }
+
+ private void checkForErrorRevokeToken(Response response) throws IOException {
+ if (response.getCode() != 200) {
+ OAuth2AccessTokenJsonExtractor.instance().generateError(response);
+ }
+ }
+
+ // ===== device Authorisation codes methods =====
+ protected OAuthRequest createDeviceAuthorizationCodesRequest(String scope) {
+ final OAuthRequest request = new OAuthRequest(Verb.POST, api.getDeviceAuthorizationEndpoint());
+ request.addParameter(OAuthConstants.CLIENT_ID, getApiKey());
+ if (scope != null) {
+ request.addParameter(OAuthConstants.SCOPE, scope);
+ } else if (defaultScope != null) {
+ request.addParameter(OAuthConstants.SCOPE, defaultScope);
+ }
+
+ logRequestWithParams("Device Authorization Codes", request);
+
+ return request;
+ }
+
+ /**
+ * Requests a set of verification codes from the authorization server with the default scope
+ *
+ * @see RFC 8628
+ *
+ * @return DeviceAuthorization
+ * @throws InterruptedException InterruptedException
+ * @throws ExecutionException ExecutionException
+ * @throws IOException IOException
+ */
+ public DeviceAuthorization getDeviceAuthorizationCodes()
+ throws InterruptedException, ExecutionException, IOException {
+ return getDeviceAuthorizationCodes((String) null);
+ }
+
+ /**
+ * Requests a set of verification codes from the authorization server
+ *
+ * @see RFC 8628
+ *
+ * @param scope scope
+ * @return DeviceAuthorization
+ * @throws InterruptedException InterruptedException
+ * @throws ExecutionException ExecutionException
+ * @throws IOException IOException
+ */
+ public DeviceAuthorization getDeviceAuthorizationCodes(String scope)
+ throws InterruptedException, ExecutionException, IOException {
+ final OAuthRequest request = createDeviceAuthorizationCodesRequest(scope);
+
+ try (Response response = execute(request)) {
+ if (isDebug()) {
+ log("got DeviceAuthorizationCodes response");
+ log("response status code: %s", response.getCode());
+ log("response body: %s", response.getBody());
+ }
+ return api.getDeviceAuthorizationExtractor().extract(response);
+ }
+ }
+
+ public Future getDeviceAuthorizationCodes(
+ OAuthAsyncRequestCallback callback) {
+ return getDeviceAuthorizationCodes(null, callback);
+ }
+
+ public Future getDeviceAuthorizationCodes(String scope,
+ OAuthAsyncRequestCallback callback) {
+ final OAuthRequest request = createDeviceAuthorizationCodesRequest(scope);
+
+ return execute(request, callback, new OAuthRequest.ResponseConverter() {
+ @Override
+ public DeviceAuthorization convert(Response response) throws IOException {
+ final DeviceAuthorization deviceAuthorization = api.getDeviceAuthorizationExtractor().extract(response);
+ response.close();
+ return deviceAuthorization;
+ }
+ });
+ }
+
+ public Future getDeviceAuthorizationCodesAsync() {
+ return getDeviceAuthorizationCodesAsync(null);
+ }
+
+ public Future getDeviceAuthorizationCodesAsync(String scope) {
+ return getDeviceAuthorizationCodes(scope, null);
+ }
+
+ // ===== get AccessToken Device Authorisation grant flow methods =====
+ protected OAuthRequest createAccessTokenDeviceAuthorizationGrantRequest(DeviceAuthorization deviceAuthorization) {
+ final OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
+ request.addParameter(OAuthConstants.GRANT_TYPE, "urn:ietf:params:oauth:grant-type:device_code");
+ request.addParameter("device_code", deviceAuthorization.getDeviceCode());
+ api.getClientAuthentication().addClientAuthentication(request, getApiKey(), getApiSecret());
+ return request;
+ }
+
+ /**
+ * Attempts to get a token from a server.
+ *
+ * Function {@link #pollAccessTokenDeviceAuthorizationGrant(com.github.scribejava.core.model.DeviceAuthorization)}
+ * is usually used instead of this.
+ *
+ * @param deviceAuthorization deviceAuthorization
+ * @return token
+ *
+ * @throws java.lang.InterruptedException InterruptedException
+ * @throws java.util.concurrent.ExecutionException ExecutionException
+ * @throws java.io.IOException IOException
+ * @throws OAuth2AccessTokenErrorResponse If {@link OAuth2AccessTokenErrorResponse#getError()} is
+ * {@link com.github.scribejava.core.oauth2.OAuth2Error#AUTHORIZATION_PENDING} or
+ * {@link com.github.scribejava.core.oauth2.OAuth2Error#SLOW_DOWN}, another attempt should be made after a while.
+ *
+ * @see #getDeviceAuthorizationCodes()
+ */
+ public OAuth2AccessToken getAccessTokenDeviceAuthorizationGrant(DeviceAuthorization deviceAuthorization)
+ throws InterruptedException, ExecutionException, IOException {
+ final OAuthRequest request = createAccessTokenDeviceAuthorizationGrantRequest(deviceAuthorization);
+
+ try (Response response = execute(request)) {
+ if (isDebug()) {
+ log("got AccessTokenDeviceAuthorizationGrant response");
+ log("response status code: %s", response.getCode());
+ log("response body: %s", response.getBody());
+ }
+ return api.getAccessTokenExtractor().extract(response);
+ }
+ }
+
+ public Future getAccessTokenDeviceAuthorizationGrant(DeviceAuthorization deviceAuthorization,
+ OAuthAsyncRequestCallback callback) {
+ final OAuthRequest request = createAccessTokenDeviceAuthorizationGrantRequest(deviceAuthorization);
+
+ return execute(request, callback, new OAuthRequest.ResponseConverter() {
+ @Override
+ public OAuth2AccessToken convert(Response response) throws IOException {
+ final OAuth2AccessToken accessToken = api.getAccessTokenExtractor().extract(response);
+ response.close();
+ return accessToken;
+ }
+ });
+ }
+
+ public Future getAccessTokenDeviceAuthorizationGrantAsync(
+ DeviceAuthorization deviceAuthorization) {
+ return getAccessTokenDeviceAuthorizationGrant(deviceAuthorization, null);
+ }
+
+ /**
+ * Periodically tries to get a token from a server (waiting for the user to give consent). Sync only version. No
+ * Async variants yet, one should implement async scenarios themselves.
+ *
+ * @param deviceAuthorization deviceAuthorization
+ * @return token
+ * @throws java.lang.InterruptedException InterruptedException
+ * @throws java.util.concurrent.ExecutionException ExecutionException
+ * @throws java.io.IOException IOException
+ * @throws OAuth2AccessTokenErrorResponse Indicates OAuth error.
+ *
+ * @see #getDeviceAuthorizationCodes()
+ */
+ public OAuth2AccessToken pollAccessTokenDeviceAuthorizationGrant(DeviceAuthorization deviceAuthorization)
+ throws InterruptedException, ExecutionException, IOException {
+ long intervalMillis = deviceAuthorization.getIntervalSeconds() * 1000;
+ while (true) {
+ try {
+ return getAccessTokenDeviceAuthorizationGrant(deviceAuthorization);
+ } catch (OAuth2AccessTokenErrorResponse e) {
+ if (e.getError() != OAuth2Error.AUTHORIZATION_PENDING) {
+ if (e.getError() == OAuth2Error.SLOW_DOWN) {
+ intervalMillis += 5000;
+ } else {
+ throw e;
+ }
+ }
+ }
+ Thread.sleep(intervalMillis);
+ }
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuthService.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuthService.java
new file mode 100644
index 000000000..996abee6f
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuthService.java
@@ -0,0 +1,148 @@
+package com.github.scribejava.core.oauth;
+
+import com.github.scribejava.core.httpclient.HttpClientProvider;
+import com.github.scribejava.core.httpclient.HttpClient;
+import com.github.scribejava.core.httpclient.HttpClientConfig;
+import com.github.scribejava.core.httpclient.jdk.JDKHttpClient;
+import com.github.scribejava.core.httpclient.jdk.JDKHttpClientConfig;
+import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Response;
+import java.io.Closeable;
+import java.io.File;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ServiceLoader;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+public abstract class OAuthService implements Closeable {
+
+ private final String apiKey;
+ private final String apiSecret;
+ private final String callback;
+ private final String userAgent;
+ private final HttpClient httpClient;
+ private final OutputStream debugStream;
+
+ public OAuthService(String apiKey, String apiSecret, String callback, OutputStream debugStream,
+ String userAgent, HttpClientConfig httpClientConfig, HttpClient httpClient) {
+ this.apiKey = apiKey;
+ this.apiSecret = apiSecret;
+ this.callback = callback;
+ this.debugStream = debugStream;
+ this.userAgent = userAgent;
+
+ if (httpClientConfig == null && httpClient == null) {
+ this.httpClient = new JDKHttpClient(JDKHttpClientConfig.defaultConfig());
+ } else {
+ this.httpClient = httpClient == null ? getClient(httpClientConfig) : httpClient;
+ }
+ }
+
+ private static HttpClient getClient(HttpClientConfig config) {
+ for (HttpClientProvider provider : ServiceLoader.load(HttpClientProvider.class)) {
+ final HttpClient client = provider.createClient(config);
+ if (client != null) {
+ return client;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ httpClient.close();
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public String getApiSecret() {
+ return apiSecret;
+ }
+
+ public String getCallback() {
+ return callback;
+ }
+
+ /**
+ * Returns the OAuth version of the service.
+ *
+ * @return OAuth version as string
+ */
+ public abstract String getVersion();
+
+ public Future executeAsync(OAuthRequest request) {
+ return execute(request, null);
+ }
+
+ public Future execute(OAuthRequest request, OAuthAsyncRequestCallback callback) {
+ return execute(request, callback, null);
+ }
+
+ public Future execute(OAuthRequest request, OAuthAsyncRequestCallback callback,
+ OAuthRequest.ResponseConverter converter) {
+
+ final File filePayload = request.getFilePayload();
+ if (filePayload != null) {
+ return httpClient.executeAsync(userAgent, request.getHeaders(), request.getVerb(), request.getCompleteUrl(),
+ filePayload, callback, converter);
+ } else if (request.getStringPayload() != null) {
+ return httpClient.executeAsync(userAgent, request.getHeaders(), request.getVerb(), request.getCompleteUrl(),
+ request.getStringPayload(), callback, converter);
+ } else {
+ return httpClient.executeAsync(userAgent, request.getHeaders(), request.getVerb(), request.getCompleteUrl(),
+ request.getByteArrayPayload(), callback, converter);
+ }
+ }
+
+ public Response execute(OAuthRequest request) throws InterruptedException, ExecutionException, IOException {
+ final File filePayload = request.getFilePayload();
+ if (filePayload != null) {
+ return httpClient.execute(userAgent, request.getHeaders(), request.getVerb(), request.getCompleteUrl(),
+ filePayload);
+ } else if (request.getStringPayload() != null) {
+ return httpClient.execute(userAgent, request.getHeaders(), request.getVerb(), request.getCompleteUrl(),
+ request.getStringPayload());
+ } else if (request.getMultipartPayload() != null) {
+ return httpClient.execute(userAgent, request.getHeaders(), request.getVerb(), request.getCompleteUrl(),
+ request.getMultipartPayload());
+ } else {
+ return httpClient.execute(userAgent, request.getHeaders(), request.getVerb(), request.getCompleteUrl(),
+ request.getByteArrayPayload());
+ }
+ }
+
+ /**
+ * No need to wrap usages in {@link #isDebug()}.
+ *
+ * @param message message to log
+ */
+ public void log(String message) {
+ if (debugStream != null) {
+ log(message, (Object[]) null);
+ }
+ }
+
+ /**
+ * Wrap usages in {@link #isDebug()}. It was made for optimization - to not calculate "params" in production mode.
+ *
+ * @param messagePattern messagePattern
+ * @param params params
+ */
+ public void log(String messagePattern, Object... params) {
+ final String message = String.format(messagePattern, params) + '\n';
+ try {
+ debugStream.write(message.getBytes("UTF8"));
+ } catch (IOException | RuntimeException e) {
+ throw new RuntimeException("there were problems while writting to the debug stream", e);
+ }
+ }
+
+ protected boolean isDebug() {
+ return debugStream != null;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/OAuth2Error.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/OAuth2Error.java
new file mode 100644
index 000000000..b1422bcae
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/OAuth2Error.java
@@ -0,0 +1,108 @@
+package com.github.scribejava.core.oauth2;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public enum OAuth2Error {
+ /**
+ * @see RFC 6749, 4.1.2.1 Error Response
+ * @see RFC 6749, 4.2.2.1 Error Response
+ * @see RFC 6749, 5.2 Error Response
+ * @see RFC 6750, 6.2. OAuth Extensions Error
+ * Registration
+ */
+ INVALID_REQUEST("invalid_request"),
+ /**
+ * @see RFC 6749, 4.1.2.1 Error Response
+ * @see RFC 6749, 4.2.2.1 Error Response
+ * @see RFC 6749, 5.2 Error Response
+ */
+ UNAUTHORIZED_CLIENT("unauthorized_client"),
+ /**
+ * @see RFC 6749, 4.1.2.1 Error Response
+ * @see RFC 6749, 4.2.2.1 Error Response
+ * @see RFC 8628, 3.5. Device Access Token Response
+ */
+ ACCESS_DENIED("access_denied"),
+ /**
+ * @see RFC 6749, 4.1.2.1 Error Response
+ * @see RFC 6749, 4.2.2.1 Error Response
+ */
+ UNSUPPORTED_RESPONSE_TYPE("unsupported_response_type"),
+ /**
+ * @see RFC 6749, 4.1.2.1 Error Response
+ * @see RFC 6749, 4.2.2.1 Error Response
+ * @see RFC 6749, 5.2 Error Response
+ */
+ INVALID_SCOPE("invalid_scope"),
+ /**
+ * @see RFC 6749, 4.1.2.1 Error Response
+ * @see RFC 6749, 4.2.2.1 Error Response
+ */
+ SERVER_ERROR("server_error"),
+ /**
+ * @see RFC 6749, 4.1.2.1 Error Response
+ * @see RFC 6749, 4.2.2.1 Error Response
+ */
+ TEMPORARILY_UNAVAILABLE("temporarily_unavailable"),
+ /**
+ * @see RFC 6749, 5.2 Error Response
+ */
+ INVALID_CLIENT("invalid_client"),
+ /**
+ * @see RFC 6749, 5.2 Error Response
+ */
+ INVALID_GRANT("invalid_grant"),
+ /**
+ * @see RFC 6749, 5.2 Error Response
+ */
+ UNSUPPORTED_GRANT_TYPE("unsupported_grant_type"),
+ /**
+ * @see RFC 6750, 6.2. OAuth Extensions Error
+ * Registration
+ */
+ INVALID_TOKEN("invalid_token"),
+ /**
+ * @see RFC 6750, 6.2. OAuth Extensions Error
+ * Registration
+ */
+ INSUFFICIENT_SCOPE("insufficient_scope"),
+ /**
+ * @see RFC 7009, 4.1. OAuth Extensions Error
+ * Registration
+ */
+ UNSUPPORTED_TOKEN_TYPE("unsupported_token_type"),
+ /**
+ * @see RFC 8628, 3.5. Device Access Token Response
+ */
+ AUTHORIZATION_PENDING("authorization_pending"),
+ /**
+ * @see RFC 8628, 3.5. Device Access Token Response
+ */
+ SLOW_DOWN("slow_down"),
+ /**
+ * @see RFC 8628, 3.5. Device Access Token Response
+ */
+ EXPIRED_TOKEN("expired_token");
+
+ private static final Set VALUES = EnumSet.allOf(OAuth2Error.class);
+
+ private final String errorString;
+
+ OAuth2Error(String errorString) {
+ this.errorString = errorString;
+ }
+
+ public static OAuth2Error parseFrom(String errorString) {
+ for (OAuth2Error error : VALUES) {
+ if (error.errorString.equals(errorString)) {
+ return error;
+ }
+ }
+ throw new IllegalArgumentException("there is no knowlege about '" + errorString + "' Error");
+ }
+
+ public String getErrorString() {
+ return errorString;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignature.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignature.java
new file mode 100644
index 000000000..61739e93e
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignature.java
@@ -0,0 +1,12 @@
+package com.github.scribejava.core.oauth2.bearersignature;
+
+import com.github.scribejava.core.model.OAuthRequest;
+
+/**
+ * Represents
+ * 2. Authenticated Requests
+ * https://tools.ietf.org/html/rfc6750#section-2
+ */
+public interface BearerSignature {
+ void signRequest(String accessToken, OAuthRequest request);
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignatureAuthorizationRequestHeaderField.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignatureAuthorizationRequestHeaderField.java
new file mode 100644
index 000000000..98196a708
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignatureAuthorizationRequestHeaderField.java
@@ -0,0 +1,29 @@
+package com.github.scribejava.core.oauth2.bearersignature;
+
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+
+/**
+ * 2.1. Authorization Request Header Field
+ * https://tools.ietf.org/html/rfc6750#section-2.1
+ */
+public class BearerSignatureAuthorizationRequestHeaderField implements BearerSignature {
+
+ protected BearerSignatureAuthorizationRequestHeaderField() {
+ }
+
+ private static class InstanceHolder {
+
+ private static final BearerSignatureAuthorizationRequestHeaderField INSTANCE
+ = new BearerSignatureAuthorizationRequestHeaderField();
+ }
+
+ public static BearerSignatureAuthorizationRequestHeaderField instance() {
+ return InstanceHolder.INSTANCE;
+ }
+
+ @Override
+ public void signRequest(String accessToken, OAuthRequest request) {
+ request.addHeader(OAuthConstants.HEADER, "Bearer " + accessToken);
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignatureURIQueryParameter.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignatureURIQueryParameter.java
new file mode 100644
index 000000000..72cc64b43
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/bearersignature/BearerSignatureURIQueryParameter.java
@@ -0,0 +1,27 @@
+package com.github.scribejava.core.oauth2.bearersignature;
+
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+
+/**
+ * 2.3. URI Query Parameter
+ * https://tools.ietf.org/html/rfc6750#section-2.3
+ */
+public class BearerSignatureURIQueryParameter implements BearerSignature {
+ protected BearerSignatureURIQueryParameter() {
+ }
+
+ private static class InstanceHolder {
+
+ private static final BearerSignatureURIQueryParameter INSTANCE = new BearerSignatureURIQueryParameter();
+ }
+
+ public static BearerSignatureURIQueryParameter instance() {
+ return InstanceHolder.INSTANCE;
+ }
+
+ @Override
+ public void signRequest(String accessToken, OAuthRequest request) {
+ request.addQuerystringParameter(OAuthConstants.ACCESS_TOKEN, accessToken);
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/ClientAuthentication.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/ClientAuthentication.java
new file mode 100644
index 000000000..54fdb5de6
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/ClientAuthentication.java
@@ -0,0 +1,17 @@
+package com.github.scribejava.core.oauth2.clientauthentication;
+
+import com.github.scribejava.core.model.OAuthRequest;
+
+/**
+ * Represents
+ * 2.3. Client Authentication
+ * https://tools.ietf.org/html/rfc6749#section-2.3
+ *
+ * just implement this interface to implement "2.3.2. Other Authentication Methods"
+ * https://tools.ietf.org/html/rfc6749#section-2.3.2
+ *
+ */
+public interface ClientAuthentication {
+
+ void addClientAuthentication(OAuthRequest request, String apiKey, String apiSecret);
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/HttpBasicAuthenticationScheme.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/HttpBasicAuthenticationScheme.java
new file mode 100644
index 000000000..3931f6f7b
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/HttpBasicAuthenticationScheme.java
@@ -0,0 +1,37 @@
+package com.github.scribejava.core.oauth2.clientauthentication;
+
+import com.github.scribejava.core.base64.Base64;
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+import java.nio.charset.Charset;
+
+/**
+ * 2.3. Client Authentication
+ * 2.3.1. Client Password
+ * https://tools.ietf.org/html/rfc6749#section-2.3.1
+ *
+ * НTTP Basic authentication scheme
+ */
+public class HttpBasicAuthenticationScheme implements ClientAuthentication {
+
+ protected HttpBasicAuthenticationScheme() {
+ }
+
+ private static class InstanceHolder {
+
+ private static final HttpBasicAuthenticationScheme INSTANCE = new HttpBasicAuthenticationScheme();
+ }
+
+ public static HttpBasicAuthenticationScheme instance() {
+ return InstanceHolder.INSTANCE;
+ }
+
+ @Override
+ public void addClientAuthentication(OAuthRequest request, String apiKey, String apiSecret) {
+ if (apiKey != null && apiSecret != null) {
+ request.addHeader(OAuthConstants.HEADER, OAuthConstants.BASIC + ' '
+ + Base64.encode(String.format("%s:%s", apiKey, apiSecret).getBytes(Charset.forName("UTF-8"))));
+ }
+ }
+
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/RequestBodyAuthenticationScheme.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/RequestBodyAuthenticationScheme.java
new file mode 100644
index 000000000..5e91affa7
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth2/clientauthentication/RequestBodyAuthenticationScheme.java
@@ -0,0 +1,34 @@
+package com.github.scribejava.core.oauth2.clientauthentication;
+
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+
+/**
+ * 2.3. Client Authentication
+ * 2.3.1. Client Password
+ * https://tools.ietf.org/html/rfc6749#section-2.3.1
+ *
+ * request-body authentication scheme
+ */
+public class RequestBodyAuthenticationScheme implements ClientAuthentication {
+
+ protected RequestBodyAuthenticationScheme() {
+ }
+
+ private static class InstanceHolder {
+
+ private static final RequestBodyAuthenticationScheme INSTANCE = new RequestBodyAuthenticationScheme();
+ }
+
+ public static RequestBodyAuthenticationScheme instance() {
+ return InstanceHolder.INSTANCE;
+ }
+
+ @Override
+ public void addClientAuthentication(OAuthRequest request, String apiKey, String apiSecret) {
+ request.addParameter(OAuthConstants.CLIENT_ID, apiKey);
+ if (apiSecret != null) {
+ request.addParameter(OAuthConstants.CLIENT_SECRET, apiSecret);
+ }
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCE.java b/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCE.java
new file mode 100644
index 000000000..c8d43d26e
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCE.java
@@ -0,0 +1,49 @@
+package com.github.scribejava.core.pkce;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Used to hold code_challenge, code_challenge_method and code_verifier for https://tools.ietf.org/html/rfc7636
+ */
+public class PKCE {
+
+ public static final String PKCE_CODE_CHALLENGE_METHOD_PARAM = "code_challenge_method";
+ public static final String PKCE_CODE_CHALLENGE_PARAM = "code_challenge";
+ public static final String PKCE_CODE_VERIFIER_PARAM = "code_verifier";
+
+ private String codeChallenge;
+ private PKCECodeChallengeMethod codeChallengeMethod = PKCECodeChallengeMethod.S256;
+ private String codeVerifier;
+
+ public String getCodeChallenge() {
+ return codeChallenge;
+ }
+
+ public void setCodeChallenge(String codeChallenge) {
+ this.codeChallenge = codeChallenge;
+ }
+
+ public PKCECodeChallengeMethod getCodeChallengeMethod() {
+ return codeChallengeMethod;
+ }
+
+ public void setCodeChallengeMethod(PKCECodeChallengeMethod codeChallengeMethod) {
+ this.codeChallengeMethod = codeChallengeMethod;
+ }
+
+ public String getCodeVerifier() {
+ return codeVerifier;
+ }
+
+ public void setCodeVerifier(String codeVerifier) {
+ this.codeVerifier = codeVerifier;
+ }
+
+ public Map getAuthorizationUrlParams() {
+ final Map params = new HashMap<>();
+ params.put(PKCE_CODE_CHALLENGE_PARAM, codeChallenge);
+ params.put(PKCE_CODE_CHALLENGE_METHOD_PARAM, codeChallengeMethod.name());
+ return params;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCECodeChallengeMethod.java b/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCECodeChallengeMethod.java
new file mode 100644
index 000000000..374f80336
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCECodeChallengeMethod.java
@@ -0,0 +1,24 @@
+package com.github.scribejava.core.pkce;
+
+import com.github.scribejava.core.base64.Base64;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public enum PKCECodeChallengeMethod {
+ S256 {
+ @Override
+ public String transform2CodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
+ return Base64.encodeUrlWithoutPadding(
+ MessageDigest.getInstance("SHA-256").digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)));
+ }
+ },
+ PLAIN {
+ @Override
+ public String transform2CodeChallenge(String codeVerifier) {
+ return codeVerifier;
+ }
+ };
+
+ public abstract String transform2CodeChallenge(String codeVerifier) throws NoSuchAlgorithmException;
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCEService.java b/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCEService.java
new file mode 100644
index 000000000..23a5347ec
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/pkce/PKCEService.java
@@ -0,0 +1,62 @@
+package com.github.scribejava.core.pkce;
+
+import com.github.scribejava.core.base64.Base64;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+/**
+ * Used to implement Proof Key for Code Exchange by OAuth Public Clients https://tools.ietf.org/html/rfc7636
+ *
+ */
+public class PKCEService {
+
+ private static final SecureRandom RANDOM = new SecureRandom();
+ /**
+ * number of octets to randomly generate.
+ */
+ private final int numberOFOctets;
+
+ public PKCEService(int numberOFOctets) {
+ this.numberOFOctets = numberOFOctets;
+ }
+
+ /**
+ * will create random generator with recommended params (32 octets) https://tools.ietf.org/html/rfc7636#section-4.1
+ */
+ public PKCEService() {
+ this(32);
+ }
+
+ private static class DefaultInstanceHolder {
+
+ private static final PKCEService INSTANCE = new PKCEService();
+ }
+
+ public static PKCEService defaultInstance() {
+ return DefaultInstanceHolder.INSTANCE;
+ }
+
+ public PKCE generatePKCE() {
+ final byte[] bytes = new byte[numberOFOctets];
+ RANDOM.nextBytes(bytes);
+ return generatePKCE(bytes);
+ }
+
+ public PKCE generatePKCE(byte[] randomBytes) {
+ final String codeVerifier = Base64.encodeUrlWithoutPadding(randomBytes);
+
+ final PKCE pkce = new PKCE();
+ pkce.setCodeVerifier(codeVerifier);
+ try {
+ pkce.setCodeChallenge(pkce.getCodeChallengeMethod().transform2CodeChallenge(codeVerifier));
+ } catch (NoSuchAlgorithmException nsaE) {
+ pkce.setCodeChallengeMethod(PKCECodeChallengeMethod.PLAIN);
+ try {
+ pkce.setCodeChallenge(PKCECodeChallengeMethod.PLAIN.transform2CodeChallenge(codeVerifier));
+ } catch (NoSuchAlgorithmException unrealE) {
+ throw new IllegalStateException("It's just cannot be", unrealE);
+ }
+ }
+ return pkce;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/revoke/OAuth2RevokeTokenResponseConverter.java b/scribejava-core/src/main/java/com/github/scribejava/core/revoke/OAuth2RevokeTokenResponseConverter.java
new file mode 100644
index 000000000..dc595a410
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/revoke/OAuth2RevokeTokenResponseConverter.java
@@ -0,0 +1,15 @@
+package com.github.scribejava.core.revoke;
+
+import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
+import com.github.scribejava.core.model.Response;
+import java.io.IOException;
+
+public class OAuth2RevokeTokenResponseConverter {
+
+ public Void convert(Response response) throws IOException {
+ if (response.getCode() != 200) {
+ OAuth2AccessTokenJsonExtractor.instance().generateError(response);
+ }
+ return null;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/revoke/TokenTypeHint.java b/scribejava-core/src/main/java/com/github/scribejava/core/revoke/TokenTypeHint.java
new file mode 100644
index 000000000..79de78d26
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/revoke/TokenTypeHint.java
@@ -0,0 +1,23 @@
+package com.github.scribejava.core.revoke;
+
+/**
+ *
+ * as stated in RFC 7009
+ * 2.1. Revocation Request
+ *
+ * @see RFC 7009, 2.1. Revocation Request
+ */
+public enum TokenTypeHint {
+ ACCESS_TOKEN("access_token"),
+ REFRESH_TOKEN("refresh_token");
+
+ private final String value;
+
+ TokenTypeHint(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/services/HMACSha1SignatureService.java b/scribejava-core/src/main/java/com/github/scribejava/core/services/HMACSha1SignatureService.java
new file mode 100644
index 000000000..363011a13
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/services/HMACSha1SignatureService.java
@@ -0,0 +1,54 @@
+package com.github.scribejava.core.services;
+
+import com.github.scribejava.core.base64.Base64;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import com.github.scribejava.core.exceptions.OAuthSignatureException;
+import com.github.scribejava.core.utils.OAuthEncoder;
+import com.github.scribejava.core.utils.Preconditions;
+
+/**
+ * HMAC-SHA1 implementation of {@link SignatureService} https://tools.ietf.org/html/rfc5849#section-3.4.2
+ */
+public class HMACSha1SignatureService implements SignatureService {
+
+ private static final String EMPTY_STRING = "";
+ private static final String CARRIAGE_RETURN = "\r\n";
+ private static final String UTF8 = "UTF-8";
+ private static final String HMAC_SHA1 = "HmacSHA1";
+ private static final String METHOD = "HMAC-SHA1";
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSignature(String baseString, String apiSecret, String tokenSecret) {
+ try {
+ Preconditions.checkEmptyString(baseString, "Base string can't be null or empty string");
+ Preconditions.checkNotNull(apiSecret, "Api secret can't be null");
+ return doSign(baseString, OAuthEncoder.encode(apiSecret) + '&' + OAuthEncoder.encode(tokenSecret));
+ } catch (UnsupportedEncodingException | NoSuchAlgorithmException | InvalidKeyException | RuntimeException e) {
+ throw new OAuthSignatureException(baseString, e);
+ }
+ }
+
+ private String doSign(String toSign, String keyString) throws UnsupportedEncodingException,
+ NoSuchAlgorithmException, InvalidKeyException {
+ final SecretKeySpec key = new SecretKeySpec(keyString.getBytes(UTF8), HMAC_SHA1);
+ final Mac mac = Mac.getInstance(HMAC_SHA1);
+ mac.init(key);
+ final byte[] bytes = mac.doFinal(toSign.getBytes(UTF8));
+ return Base64.encode(bytes).replace(CARRIAGE_RETURN, EMPTY_STRING);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSignatureMethod() {
+ return METHOD;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/services/PlaintextSignatureService.java b/scribejava-core/src/main/java/com/github/scribejava/core/services/PlaintextSignatureService.java
new file mode 100644
index 000000000..143348484
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/services/PlaintextSignatureService.java
@@ -0,0 +1,34 @@
+package com.github.scribejava.core.services;
+
+import com.github.scribejava.core.exceptions.OAuthSignatureException;
+import com.github.scribejava.core.utils.OAuthEncoder;
+import com.github.scribejava.core.utils.Preconditions;
+
+/**
+ * plaintext implementation of {@link SignatureService}
+ */
+public class PlaintextSignatureService implements SignatureService {
+
+ private static final String METHOD = "PLAINTEXT";
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSignature(String baseString, String apiSecret, String tokenSecret) {
+ try {
+ Preconditions.checkNotNull(apiSecret, "Api secret can't be null");
+ return OAuthEncoder.encode(apiSecret) + '&' + OAuthEncoder.encode(tokenSecret);
+ } catch (Exception e) {
+ throw new OAuthSignatureException(baseString, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSignatureMethod() {
+ return METHOD;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/services/RSASha1SignatureService.java b/scribejava-core/src/main/java/com/github/scribejava/core/services/RSASha1SignatureService.java
new file mode 100644
index 000000000..dc16eace4
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/services/RSASha1SignatureService.java
@@ -0,0 +1,50 @@
+package com.github.scribejava.core.services;
+
+import com.github.scribejava.core.base64.Base64;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import com.github.scribejava.core.exceptions.OAuthSignatureException;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * A signature service that uses the RSA-SHA1 algorithm.
+ */
+public class RSASha1SignatureService implements SignatureService {
+
+ private static final String METHOD = "RSA-SHA1";
+ private static final String RSA_SHA1 = "SHA1withRSA";
+ private static final String UTF8 = "UTF-8";
+
+ private final PrivateKey privateKey;
+
+ public RSASha1SignatureService(PrivateKey privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSignature(String baseString, String apiSecret, String tokenSecret) {
+ try {
+ final Signature signature = Signature.getInstance(RSA_SHA1);
+ signature.initSign(privateKey);
+ signature.update(baseString.getBytes(UTF8));
+ return Base64.encode(signature.sign());
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | UnsupportedEncodingException
+ | RuntimeException e) {
+ throw new OAuthSignatureException(baseString, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSignatureMethod() {
+ return METHOD;
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/services/SignatureService.java b/scribejava-core/src/main/java/com/github/scribejava/core/services/SignatureService.java
new file mode 100644
index 000000000..10c50f3ae
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/services/SignatureService.java
@@ -0,0 +1,20 @@
+package com.github.scribejava.core.services;
+
+/**
+ * Signs a base string, returning the OAuth signature https://tools.ietf.org/html/rfc5849#section-3.4
+ */
+public interface SignatureService {
+
+ /**
+ * Returns the signature
+ *
+ * @param baseString url-encoded string to sign
+ * @param apiSecret api secret for your app
+ * @param tokenSecret token secret (empty string for the request token step)
+ *
+ * @return signature
+ */
+ String getSignature(String baseString, String apiSecret, String tokenSecret);
+
+ String getSignatureMethod();
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/services/TimestampService.java b/scribejava-core/src/main/java/com/github/scribejava/core/services/TimestampService.java
new file mode 100644
index 000000000..f062b640a
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/services/TimestampService.java
@@ -0,0 +1,23 @@
+package com.github.scribejava.core.services;
+
+/**
+ * Unix epoch timestamp generator.
+ *
+ * This class is useful for stubbing in tests.
+ */
+public interface TimestampService {
+
+ /**
+ * Returns the unix epoch timestamp in seconds
+ *
+ * @return timestamp
+ */
+ String getTimestampInSeconds();
+
+ /**
+ * Returns a nonce (unique value for each request)
+ *
+ * @return nonce
+ */
+ String getNonce();
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/services/TimestampServiceImpl.java b/scribejava-core/src/main/java/com/github/scribejava/core/services/TimestampServiceImpl.java
new file mode 100644
index 000000000..76109f10e
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/services/TimestampServiceImpl.java
@@ -0,0 +1,60 @@
+package com.github.scribejava.core.services;
+
+import java.util.Random;
+
+/**
+ * Implementation of {@link TimestampService} using plain java classes.
+ */
+public class TimestampServiceImpl implements TimestampService {
+
+ private Timer timer;
+
+ /**
+ * Default constructor.
+ */
+ public TimestampServiceImpl() {
+ timer = new Timer();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getNonce() {
+ final Long ts = getTs();
+ return String.valueOf(ts + timer.getRandomInteger());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getTimestampInSeconds() {
+ return String.valueOf(getTs());
+ }
+
+ private Long getTs() {
+ return timer.getMilis() / 1000;
+ }
+
+ void setTimer(Timer timer) {
+ this.timer = timer;
+ }
+
+ /**
+ * Inner class that uses {@link System} for generating the timestamps.
+ */
+ static class Timer {
+
+ private final Random rand = new Random();
+
+ Long getMilis() {
+ return System.currentTimeMillis();
+ }
+
+ Integer getRandomInteger() {
+ return rand.nextInt();
+ }
+ }
+
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/utils/OAuthEncoder.java b/scribejava-core/src/main/java/com/github/scribejava/core/utils/OAuthEncoder.java
new file mode 100644
index 000000000..83918e7af
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/utils/OAuthEncoder.java
@@ -0,0 +1,51 @@
+package com.github.scribejava.core.utils;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+import com.github.scribejava.core.exceptions.OAuthException;
+
+public abstract class OAuthEncoder {
+
+ private static final String CHARSET = "UTF-8";
+ private static final Map ENCODING_RULES;
+
+ static {
+ final Map rules = new HashMap<>();
+ rules.put("*", "%2A");
+ rules.put("+", "%20");
+ rules.put("%7E", "~");
+ ENCODING_RULES = Collections.unmodifiableMap(rules);
+ }
+
+ public static String encode(String plain) {
+ Preconditions.checkNotNull(plain, "Cannot encode null object");
+ String encoded;
+ try {
+ encoded = URLEncoder.encode(plain, CHARSET);
+ } catch (UnsupportedEncodingException uee) {
+ throw new OAuthException("Charset not found while encoding string: " + CHARSET, uee);
+ }
+ for (Map.Entry rule : ENCODING_RULES.entrySet()) {
+ encoded = applyRule(encoded, rule.getKey(), rule.getValue());
+ }
+ return encoded;
+ }
+
+ private static String applyRule(String encoded, String toReplace, String replacement) {
+ return encoded.replaceAll(Pattern.quote(toReplace), replacement);
+ }
+
+ public static String decode(String encoded) {
+ Preconditions.checkNotNull(encoded, "Cannot decode null object");
+ try {
+ return URLDecoder.decode(encoded, CHARSET);
+ } catch (UnsupportedEncodingException uee) {
+ throw new OAuthException("Charset not found while decoding string: " + CHARSET, uee);
+ }
+ }
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/utils/Preconditions.java b/scribejava-core/src/main/java/com/github/scribejava/core/utils/Preconditions.java
new file mode 100644
index 000000000..19e1df083
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/utils/Preconditions.java
@@ -0,0 +1,53 @@
+package com.github.scribejava.core.utils;
+
+/**
+ * Utils for checking preconditions and invariants
+ */
+public abstract class Preconditions {
+
+ private static final String DEFAULT_MESSAGE = "Received an invalid parameter";
+
+ /**
+ * Checks that an object is not null.
+ *
+ * @param object any object
+ * @param errorMsg error message
+ *
+ * @throws IllegalArgumentException if the object is null
+ */
+ public static void checkNotNull(Object object, String errorMsg) {
+ check(object != null, errorMsg);
+ }
+
+ /**
+ * Checks that a string is not null or empty
+ *
+ * @param string any string
+ * @param errorMsg error message
+ *
+ * @throws IllegalArgumentException if the string is null or empty
+ */
+ public static void checkEmptyString(String string, String errorMsg) {
+ check(hasText(string), errorMsg);
+ }
+
+ public static boolean hasText(String str) {
+ if (str == null || str.isEmpty()) {
+ return false;
+ }
+ final int strLen = str.length();
+ for (int i = 0; i < strLen; i++) {
+ if (!Character.isWhitespace(str.charAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void check(boolean requirements, String error) {
+ if (!requirements) {
+ throw new IllegalArgumentException(hasText(error) ? error : DEFAULT_MESSAGE);
+ }
+ }
+
+}
diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/utils/StreamUtils.java b/scribejava-core/src/main/java/com/github/scribejava/core/utils/StreamUtils.java
new file mode 100644
index 000000000..7a86318d2
--- /dev/null
+++ b/scribejava-core/src/main/java/com/github/scribejava/core/utils/StreamUtils.java
@@ -0,0 +1,49 @@
+package com.github.scribejava.core.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Utils to deal with Streams.
+ */
+public abstract class StreamUtils {
+
+ /**
+ * Returns the stream contents as an UTF-8 encoded string
+ *
+ * @param is input stream
+ * @return string contents
+ * @throws java.io.IOException in any. SocketTimeout in example
+ */
+ public static String getStreamContents(InputStream is) throws IOException {
+ Preconditions.checkNotNull(is, "Cannot get String from a null object");
+ final char[] buffer = new char[0x10000];
+ final StringBuilder out = new StringBuilder();
+ try (Reader in = new InputStreamReader(is, "UTF-8")) {
+ int read;
+ do {
+ read = in.read(buffer, 0, buffer.length);
+ if (read > 0) {
+ out.append(buffer, 0, read);
+ }
+ } while (read >= 0);
+ }
+ return out.toString();
+ }
+
+ /**
+ * Return String content from a gzip stream
+ *
+ * @param is input stream
+ * @return string contents
+ * @throws java.io.IOException in any. SocketTimeout in example
+ */
+ public static String getGzipStreamContents(InputStream is) throws IOException {
+ Preconditions.checkNotNull(is, "Cannot get String from a null object");
+ final GZIPInputStream gis = new GZIPInputStream(is);
+ return getStreamContents(gis);
+ }
+}
diff --git a/scribejava-core/src/main/resources/META-INF/services/com.github.scribejava.core.httpclient.HttpClientProvider b/scribejava-core/src/main/resources/META-INF/services/com.github.scribejava.core.httpclient.HttpClientProvider
new file mode 100644
index 000000000..7df053c24
--- /dev/null
+++ b/scribejava-core/src/main/resources/META-INF/services/com.github.scribejava.core.httpclient.HttpClientProvider
@@ -0,0 +1,2 @@
+com.github.scribejava.core.httpclient.jdk.JDKHttpProvider
+
diff --git a/scribejava-core/src/test/java/com/github/scribejava/core/AbstractClientTest.java b/scribejava-core/src/test/java/com/github/scribejava/core/AbstractClientTest.java
new file mode 100644
index 000000000..c1bc1967e
--- /dev/null
+++ b/scribejava-core/src/test/java/com/github/scribejava/core/AbstractClientTest.java
@@ -0,0 +1,264 @@
+package com.github.scribejava.core;
+
+import com.github.scribejava.core.httpclient.HttpClient;
+import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Response;
+import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import com.github.scribejava.core.oauth.OAuthService;
+import com.github.scribejava.core.utils.StreamUtils;
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotNull;
+
+public abstract class AbstractClientTest {
+
+ private OAuthService oAuthService;
+
+ private static class TestCallback implements OAuthAsyncRequestCallback {
+
+ private Response response;
+
+ @Override
+ public void onCompleted(Response response) {
+ this.response = response;
+ }
+
+ @Override
+ public void onThrowable(Throwable throwable) {
+ }
+
+ public Response getResponse() {
+ return response;
+ }
+ }
+
+ @Before
+ public void setUp() {
+ oAuthService = new OAuth20Service(null, "test", "test", null, null, null, System.out, null, null,
+ createNewClient());
+ }
+
+ @After
+ public void shutDown() throws Exception {
+ oAuthService.close();
+ }
+
+ protected abstract HttpClient createNewClient();
+
+ @Test
+ public void shouldSendGetRequest() throws Exception {
+ final String expectedResponseBody = "response body for test shouldSendGetRequest";
+
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setBody(expectedResponseBody));
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.GET, baseUrl.toString());
+
+ try (Response response = oAuthService.execute(request, null).get(30, TimeUnit.SECONDS)) {
+ assertEquals(expectedResponseBody, response.getBody());
+ }
+
+ final RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("GET", recordedRequest.getMethod());
+
+ server.shutdown();
+ }
+
+ @Test
+ public void shouldSendPostWithApplicationXWwwFormUrlencodedRequestContentTypeHeader() throws Exception {
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse());
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.POST, baseUrl.toString());
+ oAuthService.execute(request, null).get(30, TimeUnit.SECONDS).close();
+
+ final RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals(HttpClient.DEFAULT_CONTENT_TYPE, recordedRequest.getHeader(HttpClient.CONTENT_TYPE));
+
+ server.shutdown();
+ }
+
+ @Test
+ public void shouldSendPostRequestWithEmptyBody() throws Exception {
+ final String expectedResponseBody = "response body for test shouldSendPostRequest";
+ final String expectedRequestBody = "";
+
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setBody(expectedResponseBody));
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.POST, baseUrl.toString());
+ try (Response response = oAuthService.execute(request, null).get(30, TimeUnit.SECONDS)) {
+ assertEquals(expectedResponseBody, response.getBody());
+ }
+
+ final RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals(expectedRequestBody, recordedRequest.getBody().readUtf8());
+ assertEquals(HttpClient.DEFAULT_CONTENT_TYPE, recordedRequest.getHeader(HttpClient.CONTENT_TYPE));
+
+ server.shutdown();
+ }
+
+ @Test
+ public void shouldSendPostRequestWithStringBody() throws Exception {
+ final String expectedResponseBody = "response body for test shouldSendPostRequest";
+ final String expectedRequestBody = "request body";
+
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setBody(expectedResponseBody));
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.POST, baseUrl.toString());
+ request.setPayload(expectedRequestBody);
+ try (Response response = oAuthService.execute(request, null).get(30, TimeUnit.SECONDS)) {
+ assertEquals(expectedResponseBody, response.getBody());
+ }
+
+ final RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals(expectedRequestBody, recordedRequest.getBody().readUtf8());
+ final String contentTypeHeader = recordedRequest.getHeader(HttpClient.CONTENT_TYPE);
+ assertNotNull(contentTypeHeader);
+ assertTrue(contentTypeHeader.startsWith(HttpClient.DEFAULT_CONTENT_TYPE));
+
+ server.shutdown();
+ }
+
+ @Test
+ public void shouldSendPostRequestWithByteBodyBody() throws Exception {
+ final String expectedResponseBody = "response body for test shouldSendPostRequest";
+ final String expectedRequestBody = "request body";
+
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setBody(expectedResponseBody));
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.POST, baseUrl.toString());
+ request.setPayload(expectedRequestBody.getBytes());
+ try (Response response = oAuthService.execute(request, null).get(30, TimeUnit.SECONDS)) {
+ assertEquals(expectedResponseBody, response.getBody());
+ }
+
+ final RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals(expectedRequestBody, recordedRequest.getBody().readUtf8());
+ assertEquals(HttpClient.DEFAULT_CONTENT_TYPE, recordedRequest.getHeader(HttpClient.CONTENT_TYPE));
+
+ server.shutdown();
+ }
+
+ @Test
+ public void shouldSendPostRequestWithBodyParamsBody() throws Exception {
+ final String expectedResponseBody = "response body for test shouldSendPostRequest";
+ final String expectedRequestBodyParamName = "request_body_param_name";
+ final String expectedRequestBodyParamValue = "request_body_param_value";
+ final String expectedRequestBody = expectedRequestBodyParamName + '=' + expectedRequestBodyParamValue;
+
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setBody(expectedResponseBody));
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.POST, baseUrl.toString());
+ request.addBodyParameter(expectedRequestBodyParamName, expectedRequestBodyParamValue);
+ try (Response response = oAuthService.execute(request, null).get(30, TimeUnit.SECONDS)) {
+ assertEquals(expectedResponseBody, response.getBody());
+ }
+
+ final RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals(expectedRequestBody, recordedRequest.getBody().readUtf8());
+ assertEquals(HttpClient.DEFAULT_CONTENT_TYPE, recordedRequest.getHeader(HttpClient.CONTENT_TYPE));
+
+ server.shutdown();
+ }
+
+ @Test
+ public void shouldReadResponseStream() throws Exception {
+ final String expectedResponseBody = "response body for test shouldReadResponseStream";
+
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setBody(expectedResponseBody));
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.GET, baseUrl.toString());
+ try (Response response = oAuthService.execute(request, null).get(30, TimeUnit.SECONDS)) {
+ assertEquals(expectedResponseBody, StreamUtils.getStreamContents(response.getStream()));
+ }
+
+ final RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("GET", recordedRequest.getMethod());
+
+ server.shutdown();
+ }
+
+ @Test
+ public void shouldCallCallback() throws Exception {
+ final String expectedResponseBody = "response body for test shouldCallCallback";
+
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setBody(expectedResponseBody));
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.GET, baseUrl.toString());
+
+ final TestCallback callback = new TestCallback();
+ oAuthService.execute(request, callback).get();
+
+ assertEquals(expectedResponseBody, callback.getResponse().getBody());
+
+ server.shutdown();
+ }
+
+ @Test
+ public void shouldPassErrors() throws Exception {
+
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setResponseCode(500));
+ server.start();
+
+ final HttpUrl baseUrl = server.url("/testUrl");
+
+ final OAuthRequest request = new OAuthRequest(Verb.GET, baseUrl.toString());
+
+ final TestCallback callback = new TestCallback();
+ try (Response response = oAuthService.execute(request, callback).get()) {
+
+ assertEquals(500, response.getCode());
+ assertEquals(500, callback.getResponse().getCode());
+ }
+
+ server.shutdown();
+ }
+}
diff --git a/scribejava-core/src/test/java/com/github/scribejava/core/ObjectMother.java b/scribejava-core/src/test/java/com/github/scribejava/core/ObjectMother.java
new file mode 100644
index 000000000..07e4081ec
--- /dev/null
+++ b/scribejava-core/src/test/java/com/github/scribejava/core/ObjectMother.java
@@ -0,0 +1,62 @@
+package com.github.scribejava.core;
+
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Verb;
+
+public abstract class ObjectMother {
+
+ public static OAuthRequest createSampleOAuthRequest() {
+ final OAuthRequest request = new OAuthRequest(Verb.GET, "http://example.com");
+ request.addOAuthParameter(OAuthConstants.TIMESTAMP, "123456");
+ request.addOAuthParameter(OAuthConstants.CONSUMER_KEY, "AS#$^*@&");
+ request.addOAuthParameter(OAuthConstants.CALLBACK, "http://example/callback");
+ request.addOAuthParameter(OAuthConstants.SIGNATURE, "OAuth-Signature");
+ return request;
+ }
+
+ public static OAuthRequest createSampleOAuthRequestPort80() {
+ final OAuthRequest request = new OAuthRequest(Verb.GET, "http://example.com:80");
+ request.addOAuthParameter(OAuthConstants.TIMESTAMP, "123456");
+ request.addOAuthParameter(OAuthConstants.CONSUMER_KEY, "AS#$^*@&");
+ request.addOAuthParameter(OAuthConstants.CALLBACK, "http://example/callback");
+ request.addOAuthParameter(OAuthConstants.SIGNATURE, "OAuth-Signature");
+ return request;
+ }
+
+ public static OAuthRequest createSampleOAuthRequestPort80v2() {
+ final OAuthRequest request = new OAuthRequest(Verb.GET, "http://example.com:80/test");
+ request.addOAuthParameter(OAuthConstants.TIMESTAMP, "123456");
+ request.addOAuthParameter(OAuthConstants.CONSUMER_KEY, "AS#$^*@&");
+ request.addOAuthParameter(OAuthConstants.CALLBACK, "http://example/callback");
+ request.addOAuthParameter(OAuthConstants.SIGNATURE, "OAuth-Signature");
+ return request;
+ }
+
+ public static OAuthRequest createSampleOAuthRequestPort8080() {
+ final OAuthRequest request = new OAuthRequest(Verb.GET, "http://example.com:8080");
+ request.addOAuthParameter(OAuthConstants.TIMESTAMP, "123456");
+ request.addOAuthParameter(OAuthConstants.CONSUMER_KEY, "AS#$^*@&");
+ request.addOAuthParameter(OAuthConstants.CALLBACK, "http://example/callback");
+ request.addOAuthParameter(OAuthConstants.SIGNATURE, "OAuth-Signature");
+ return request;
+ }
+
+ public static OAuthRequest createSampleOAuthRequestPort443() {
+ final OAuthRequest request = new OAuthRequest(Verb.GET, "https://example.com:443");
+ request.addOAuthParameter(OAuthConstants.TIMESTAMP, "123456");
+ request.addOAuthParameter(OAuthConstants.CONSUMER_KEY, "AS#$^*@&");
+ request.addOAuthParameter(OAuthConstants.CALLBACK, "http://example/callback");
+ request.addOAuthParameter(OAuthConstants.SIGNATURE, "OAuth-Signature");
+ return request;
+ }
+
+ public static OAuthRequest createSampleOAuthRequestPort443v2() {
+ final OAuthRequest request = new OAuthRequest(Verb.GET, "https://example.com:443/test");
+ request.addOAuthParameter(OAuthConstants.TIMESTAMP, "123456");
+ request.addOAuthParameter(OAuthConstants.CONSUMER_KEY, "AS#$^*@&");
+ request.addOAuthParameter(OAuthConstants.CALLBACK, "http://example/callback");
+ request.addOAuthParameter(OAuthConstants.SIGNATURE, "OAuth-Signature");
+ return request;
+ }
+}
diff --git a/scribejava-core/src/test/java/com/github/scribejava/core/base64/Base64Test.java b/scribejava-core/src/test/java/com/github/scribejava/core/base64/Base64Test.java
new file mode 100644
index 000000000..4d345b77d
--- /dev/null
+++ b/scribejava-core/src/test/java/com/github/scribejava/core/base64/Base64Test.java
@@ -0,0 +1,213 @@
+package com.github.scribejava.core.base64;
+
+import java.io.UnsupportedEncodingException;
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+public class Base64Test {
+
+ private Base64 java8Base64;
+ private Base64 commonsCodecBase64;
+ private Base64 jaxbBase64;
+ private Base64 jaxb230Base64;
+ private byte[] helloWorldBytes;
+ private byte[] helloWorldTwoLinesBytes;
+ private byte[] helloWorldTwoLinesAndNewLineBytes;
+ private byte[] helloWorldDifferentCharsBytes;
+ private byte[] bytes;
+ private byte[] allBytes;
+
+ @Before
+ public void setUp() throws UnsupportedEncodingException {
+ helloWorldBytes = "Hello World".getBytes("UTF-8");
+ helloWorldTwoLinesBytes = "Hello World\r\nNew Line2".getBytes("UTF-8");
+ helloWorldTwoLinesAndNewLineBytes = "Hello World\r\nSecond Line\r\n".getBytes("UTF-8");
+ helloWorldDifferentCharsBytes = ("`1234567890-=~!@#$%^&*()_+ёЁ\"№;:?qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP"
+ + "{}|ASDFGHJKL:ZXCVBNM<>?йфяцычувскамепинртгоьшлбщдюзж.хэъ\\ЙФЯЦЫЧУВСКАМЕПИНРТГОЬШЛБЩДЮЗЖ,ХЭЪ/\r\t\f\'"
+ + "\b\n").getBytes("UTF-8");
+ bytes = new byte[]{48, -126, 2, 118, 2, 1, 0, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 4, -126,
+ 2, 96, 48, -126, 2, 92, 2, 1, 0, 2, -127, -127, 0, -61, -48, -28, 16, -116, -58, 85, 42, -39, 54, 50, -119,
+ 18, 40, 17, 75, 51, -24, 113, -109, 38, 17, -18, 106, -60, -74, -97, 29, 82, 123, -128, -88, -34, 92, 112,
+ -57, 43, -101, 85, -47, 99, -16, 11, -95, 28, -46, 82, -104, -101, -29, -106, -106, -45, -80, 99, -93, 45,
+ -102, 107, 31, 32, -60, 13, -46, 102, 127, 81, 94, -98, -56, 117, 50, 21, 39, 5, -98, 26, -18, -30, -21,
+ 102, -78, -77, 20, 113, -55, 117, -87, -105, -10, -100, 90, -92, 31, 61, -68, -73, -121, -108, 42, 45, -10,
+ 21, 87, 118, -74, 71, -100, -37, 96, -24, 87, 102, 68, -95, -1, -14, 6, -20, -14, 32, -14, 33, -84, -123,
+ -65, 54, 3, 2, 3, 1, 0, 1, 2, -127, -128, 62, 115, -45, 41, 76, 28, -67, 113, 11, 17, -12, 16, 47, -112, 67,
+ -29, -66, 76, 118, 92, -66, 25, -99, -10, -61, -126, -109, 64, -32, -37, -82, -17, 44, -20, 66, -77, -29,
+ 62, -119, -94, 92, -61, 100, -110, 32, 5, 28, 126, -69, -55, 92, 112, 2, 88, 17, -113, 43, -82, 66, 88, 13,
+ 53, 58, 74, -65, 36, 45, 93, -63, -15, 125, -7, -44, -45, -51, -76, 86, 97, 54, -36, -49, -117, -18, 56, 54,
+ 78, 80, 119, -6, -75, 39, 16, 57, -125, -68, 42, 50, -114, 92, 6, 13, 30, -91, 53, -66, -19, -20, 88, 32,
+ -38, 36, 126, -119, -86, 47, -46, 37, 115, -49, -23, 125, -61, 75, 37, 70, 92, -122, -79, 2, 65, 0, -11,
+ -105, 91, 105, -73, 54, 97, 96, -87, -16, -15, -73, 15, 31, -80, -96, -74, -53, -54, 53, -17, -9, 39, 62,
+ 58, 51, 68, 107, 86, 111, -62, -48, -125, 117, 66, 111, -55, 27, 56, 81, -50, 96, -47, -102, -50, -83, -52,
+ -17, -20, 3, -42, -94, 11, 23, 104, 127, 29, -25, 32, 43, -41, -112, -83, -99, 2, 65, 0, -52, 29, 122, 9,
+ 49, -14, -118, 110, -79, 107, 76, -88, 4, -49, 40, 32, 59, 88, 45, -71, 62, 78, 93, -121, -123, 123, 3, 4,
+ 111, -112, 27, 12, -115, -123, 125, 39, 54, 96, -2, -46, 30, 40, -4, -119, 13, -121, 118, -23, 1, -83, -76,
+ -26, -117, -86, -79, -121, 113, -26, 33, 30, 124, 35, -16, 31, 2, 65, 0, -47, -113, 111, -81, 75, 104, -103,
+ -69, 20, 7, -57, 25, -65, 75, -7, 57, -118, 1, 102, -16, -109, 108, -64, 13, -73, 55, -37, -32, 3, -121,
+ -90, 34, -86, -87, -70, 33, 12, -25, -81, 45, 14, -1, 74, -101, -32, 84, 41, -107, 104, 60, -10, 62, -101,
+ 92, 68, 12, -124, 5, -98, 76, 10, -53, 39, 121, 2, 64, 7, 106, 102, -67, -96, -57, -20, 9, -101, 126, -121,
+ 121, 111, 59, 75, 124, -24, 75, 10, -42, 57, 18, 69, -55, -97, -86, -39, 112, 54, -47, 104, 122, 43, 70, 23,
+ 70, -18, 109, -43, -76, 50, -114, 80, -90, 118, 12, 94, -32, -106, 68, 6, 87, 125, -23, -124, -85, -92, 18,
+ -75, 79, 83, 57, 71, 7, 2, 64, 73, -64, -3, 78, -90, -122, -64, -99, -29, -71, 75, 21, -74, -24, -43, -37,
+ 116, -89, 31, -115, -30, 50, 8, 23, 79, -71, -68, -39, 36, -23, 60, 102, -90, -42, 19, -33, -102, -85, -74,
+ 103, 73, -30, 120, -15, 104, -9, 110, -24, -127, 14, -57, -44, 67, 9, 80, 120, 42, 94, 107, -81, -109, 101,
+ -1, 91};
+
+ allBytes = new byte[]{-128, -127, -126, -125, -124, -123, -122, -121, -120, -119, -118, -117, -116, -115, -114,
+ -113, -112, -111, -110, -109, -108, -107, -106, -105, -104, -103, -102, -101, -100, -99, -98, -97, -96, -95,
+ -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, -83, -82, -81, -80, -79, -78, -77, -76, -75, -74,
+ -73, -72, -71, -70, -69, -68, -67, -66, -65, -64, -63, -62, -61, -60, -59, -58, -57, -56, -55, -54, -53,
+ -52, -51, -50, -49, -48, -47, -46, -45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32,
+ -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11,
+ -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
+ 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
+ 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
+ 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
+ 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
+ 121, 122, 123, 124, 125, 126, 127};
+
+ java8Base64 = new Java8Base64();
+ commonsCodecBase64 = new CommonsCodecBase64();
+ jaxbBase64 = new JaxbBase64();
+ jaxb230Base64 = new Jaxb230Base64();
+ }
+
+ @Test
+ public void allImplementationsAreAvailable() {
+ assertTrue(Java8Base64.isAvailable());
+ assertTrue(CommonsCodecBase64.isAvailable());
+ assertTrue(JaxbBase64.isAvailable());
+ assertTrue(Jaxb230Base64.isAvailable());
+ }
+
+ @Test
+ public void testEncode() {
+ final String helloWorldEncoded = "SGVsbG8gV29ybGQ=";
+ final String helloWorldTwoLinesEncoded = "SGVsbG8gV29ybGQNCk5ldyBMaW5lMg==";
+ final String helloWorldTwoLinesAndNewLineEncoded = "SGVsbG8gV29ybGQNClNlY29uZCBMaW5lDQo=";
+ final String helloWorldDifferentCharsEncoded = "YDEyMzQ1Njc4OTAtPX4hQCMkJV4mKigpXyvRkdCBIuKEljs6P3F3ZXJ0eXVpb3B"
+ + "bXWFzZGZnaGprbDsnenhjdmJubSwuL1FXRVJUWVVJT1B7fXxBU0RGR0hKS0w6WlhDVkJOTTw+P9C50YTRj9GG0YvRh9GD0LLRgdC"
+ + "60LDQvNC10L/QuNC90YDRgtCz0L7RjNGI0LvQsdGJ0LTRjtC30LYu0YXRjdGKXNCZ0KTQr9Cm0KvQp9Cj0JLQodCa0JDQnNCV0J/"
+ + "QmNCd0KDQotCT0J7QrNCo0JvQkdCp0JTQrtCX0JYs0KXQrdCqLw0JDCcICg==";
+ final String str = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMPQ5BCMxlUq2TYy"
+ + "iRIoEUsz6HGTJhHuasS2nx1Se4Co3lxwxyubVdFj8AuhHNJSmJvjlpbTsGOjLZpr"
+ + "HyDEDdJmf1Fensh1MhUnBZ4a7uLrZrKzFHHJdamX9pxapB89vLeHlCot9hVXdrZH"
+ + "nNtg6FdmRKH/8gbs8iDyIayFvzYDAgMBAAECgYA+c9MpTBy9cQsR9BAvkEPjvkx2"
+ + "XL4ZnfbDgpNA4Nuu7yzsQrPjPomiXMNkkiAFHH67yVxwAlgRjyuuQlgNNTpKvyQt"
+ + "XcHxffnU0820VmE23M+L7jg2TlB3+rUnEDmDvCoyjlwGDR6lNb7t7Fgg2iR+iaov"
+ + "0iVzz+l9w0slRlyGsQJBAPWXW2m3NmFgqfDxtw8fsKC2y8o17/cnPjozRGtWb8LQ"
+ + "g3VCb8kbOFHOYNGazq3M7+wD1qILF2h/HecgK9eQrZ0CQQDMHXoJMfKKbrFrTKgE"
+ + "zyggO1gtuT5OXYeFewMEb5AbDI2FfSc2YP7SHij8iQ2HdukBrbTmi6qxh3HmIR58"
+ + "I/AfAkEA0Y9vr0tombsUB8cZv0v5OYoBZvCTbMANtzfb4AOHpiKqqbohDOevLQ7/"
+ + "SpvgVCmVaDz2PptcRAyEBZ5MCssneQJAB2pmvaDH7Ambfod5bztLfOhLCtY5EkXJ"
+ + "n6rZcDbRaHorRhdG7m3VtDKOUKZ2DF7glkQGV33phKukErVPUzlHBwJAScD9TqaG"
+ + "wJ3juUsVtujV23SnH43iMggXT7m82STpPGam1hPfmqu2Z0niePFo927ogQ7H1EMJ"
+ + "UHgqXmuvk2X/Ww==";
+
+ final String allBytesStr = "gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2"
+ + "+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAQIDBAUGBwg"
+ + "JCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlN"
+ + "UVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+fw==";
+
+ assertEquals(helloWorldEncoded, java8Base64.internalEncode(helloWorldBytes));
+ assertEquals(helloWorldTwoLinesEncoded, java8Base64.internalEncode(helloWorldTwoLinesBytes));
+ assertEquals(helloWorldTwoLinesAndNewLineEncoded,
+ java8Base64.internalEncode(helloWorldTwoLinesAndNewLineBytes));
+ assertEquals(helloWorldDifferentCharsEncoded, java8Base64.internalEncode(helloWorldDifferentCharsBytes));
+ assertEquals(str, java8Base64.internalEncode(bytes));
+ assertEquals(allBytesStr, java8Base64.internalEncode(allBytes));
+
+ assertEquals(helloWorldEncoded, commonsCodecBase64.internalEncode(helloWorldBytes));
+ assertEquals(helloWorldTwoLinesEncoded, commonsCodecBase64.internalEncode(helloWorldTwoLinesBytes));
+ assertEquals(helloWorldTwoLinesAndNewLineEncoded,
+ commonsCodecBase64.internalEncode(helloWorldTwoLinesAndNewLineBytes));
+ assertEquals(helloWorldDifferentCharsEncoded, commonsCodecBase64.internalEncode(helloWorldDifferentCharsBytes));
+ assertEquals(str, commonsCodecBase64.internalEncode(bytes));
+ assertEquals(allBytesStr, commonsCodecBase64.internalEncode(allBytes));
+
+ assertEquals(helloWorldEncoded, jaxbBase64.internalEncode(helloWorldBytes));
+ assertEquals(helloWorldTwoLinesEncoded, jaxbBase64.internalEncode(helloWorldTwoLinesBytes));
+ assertEquals(helloWorldTwoLinesAndNewLineEncoded, jaxbBase64.internalEncode(helloWorldTwoLinesAndNewLineBytes));
+ assertEquals(helloWorldDifferentCharsEncoded, jaxbBase64.internalEncode(helloWorldDifferentCharsBytes));
+ assertEquals(str, jaxbBase64.internalEncode(bytes));
+ assertEquals(allBytesStr, jaxbBase64.internalEncode(allBytes));
+
+ assertEquals(helloWorldEncoded, jaxb230Base64.internalEncode(helloWorldBytes));
+ assertEquals(helloWorldTwoLinesEncoded, jaxb230Base64.internalEncode(helloWorldTwoLinesBytes));
+ assertEquals(helloWorldTwoLinesAndNewLineEncoded,
+ jaxb230Base64.internalEncode(helloWorldTwoLinesAndNewLineBytes));
+ assertEquals(helloWorldDifferentCharsEncoded, jaxb230Base64.internalEncode(helloWorldDifferentCharsBytes));
+ assertEquals(str, jaxb230Base64.internalEncode(bytes));
+ assertEquals(allBytesStr, jaxb230Base64.internalEncode(allBytes));
+ }
+
+ @Test
+ public void testEncodeUrlWithoutPadding() {
+ final String helloWorldEncoded = "SGVsbG8gV29ybGQ";
+ final String helloWorldTwoLinesEncoded = "SGVsbG8gV29ybGQNCk5ldyBMaW5lMg";
+ final String helloWorldTwoLinesAndNewLineEncoded = "SGVsbG8gV29ybGQNClNlY29uZCBMaW5lDQo";
+ final String helloWorldDifferentCharsEncoded = "YDEyMzQ1Njc4OTAtPX4hQCMkJV4mKigpXyvRkdCBIuKEljs6P3F3ZXJ0eXVpb3B"
+ + "bXWFzZGZnaGprbDsnenhjdmJubSwuL1FXRVJUWVVJT1B7fXxBU0RGR0hKS0w6WlhDVkJOTTw-P9C50YTRj9GG0YvRh9GD0LLRgdC"
+ + "60LDQvNC10L_QuNC90YDRgtCz0L7RjNGI0LvQsdGJ0LTRjtC30LYu0YXRjdGKXNCZ0KTQr9Cm0KvQp9Cj0JLQodCa0JDQnNCV0J_"
+ + "QmNCd0KDQotCT0J7QrNCo0JvQkdCp0JTQrtCX0JYs0KXQrdCqLw0JDCcICg";
+ final String str = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMPQ5BCMxlUq2TYy"
+ + "iRIoEUsz6HGTJhHuasS2nx1Se4Co3lxwxyubVdFj8AuhHNJSmJvjlpbTsGOjLZpr"
+ + "HyDEDdJmf1Fensh1MhUnBZ4a7uLrZrKzFHHJdamX9pxapB89vLeHlCot9hVXdrZH"
+ + "nNtg6FdmRKH_8gbs8iDyIayFvzYDAgMBAAECgYA-c9MpTBy9cQsR9BAvkEPjvkx2"
+ + "XL4ZnfbDgpNA4Nuu7yzsQrPjPomiXMNkkiAFHH67yVxwAlgRjyuuQlgNNTpKvyQt"
+ + "XcHxffnU0820VmE23M-L7jg2TlB3-rUnEDmDvCoyjlwGDR6lNb7t7Fgg2iR-iaov"
+ + "0iVzz-l9w0slRlyGsQJBAPWXW2m3NmFgqfDxtw8fsKC2y8o17_cnPjozRGtWb8LQ"
+ + "g3VCb8kbOFHOYNGazq3M7-wD1qILF2h_HecgK9eQrZ0CQQDMHXoJMfKKbrFrTKgE"
+ + "zyggO1gtuT5OXYeFewMEb5AbDI2FfSc2YP7SHij8iQ2HdukBrbTmi6qxh3HmIR58"
+ + "I_AfAkEA0Y9vr0tombsUB8cZv0v5OYoBZvCTbMANtzfb4AOHpiKqqbohDOevLQ7_"
+ + "SpvgVCmVaDz2PptcRAyEBZ5MCssneQJAB2pmvaDH7Ambfod5bztLfOhLCtY5EkXJ"
+ + "n6rZcDbRaHorRhdG7m3VtDKOUKZ2DF7glkQGV33phKukErVPUzlHBwJAScD9TqaG"
+ + "wJ3juUsVtujV23SnH43iMggXT7m82STpPGam1hPfmqu2Z0niePFo927ogQ7H1EMJ"
+ + "UHgqXmuvk2X_Ww";
+
+ final String allBytesStr = "gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp-goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2"
+ + "-v8DBwsPExcbHyMnKy8zNzs_Q0dLT1NXW19jZ2tvc3d7f4OHi4-Tl5ufo6err7O3u7_Dx8vP09fb3-Pn6-_z9_v8AAQIDBAUGBwg"
+ + "JCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4_QEFCQ0RFRkdISUpLTE1OT1BRUlN"
+ + "UVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1-fw";
+
+ assertEquals(helloWorldEncoded, java8Base64.internalEncodeUrlWithoutPadding(helloWorldBytes));
+ assertEquals(helloWorldTwoLinesEncoded, java8Base64.internalEncodeUrlWithoutPadding(helloWorldTwoLinesBytes));
+ assertEquals(helloWorldTwoLinesAndNewLineEncoded,
+ java8Base64.internalEncodeUrlWithoutPadding(helloWorldTwoLinesAndNewLineBytes));
+ assertEquals(helloWorldDifferentCharsEncoded,
+ java8Base64.internalEncodeUrlWithoutPadding(helloWorldDifferentCharsBytes));
+ assertEquals(str, java8Base64.internalEncodeUrlWithoutPadding(bytes));
+ assertEquals(allBytesStr, java8Base64.internalEncodeUrlWithoutPadding(allBytes));
+
+ assertEquals(helloWorldEncoded, commonsCodecBase64.internalEncodeUrlWithoutPadding(helloWorldBytes));
+ assertEquals(helloWorldTwoLinesEncoded,
+ commonsCodecBase64.internalEncodeUrlWithoutPadding(helloWorldTwoLinesBytes));
+ assertEquals(helloWorldTwoLinesAndNewLineEncoded,
+ commonsCodecBase64.internalEncodeUrlWithoutPadding(helloWorldTwoLinesAndNewLineBytes));
+ assertEquals(helloWorldDifferentCharsEncoded,
+ commonsCodecBase64.internalEncodeUrlWithoutPadding(helloWorldDifferentCharsBytes));
+ assertEquals(str, commonsCodecBase64.internalEncodeUrlWithoutPadding(bytes));
+ assertEquals(allBytesStr, commonsCodecBase64.internalEncodeUrlWithoutPadding(allBytes));
+
+ assertEquals(helloWorldEncoded, jaxbBase64.internalEncodeUrlWithoutPadding(helloWorldBytes));
+ assertEquals(helloWorldTwoLinesEncoded, jaxbBase64.internalEncodeUrlWithoutPadding(helloWorldTwoLinesBytes));
+ assertEquals(helloWorldTwoLinesAndNewLineEncoded,
+ jaxbBase64.internalEncodeUrlWithoutPadding(helloWorldTwoLinesAndNewLineBytes));
+ assertEquals(helloWorldDifferentCharsEncoded,
+ jaxbBase64.internalEncodeUrlWithoutPadding(helloWorldDifferentCharsBytes));
+ assertEquals(str, jaxbBase64.internalEncodeUrlWithoutPadding(bytes));
+ assertEquals(allBytesStr, jaxbBase64.internalEncodeUrlWithoutPadding(allBytes));
+
+ assertEquals(helloWorldEncoded, jaxb230Base64.internalEncodeUrlWithoutPadding(helloWorldBytes));
+ assertEquals(helloWorldTwoLinesEncoded, jaxb230Base64.internalEncodeUrlWithoutPadding(helloWorldTwoLinesBytes));
+ assertEquals(helloWorldTwoLinesAndNewLineEncoded,
+ jaxb230Base64.internalEncodeUrlWithoutPadding(helloWorldTwoLinesAndNewLineBytes));
+ assertEquals(helloWorldDifferentCharsEncoded,
+ jaxb230Base64.internalEncodeUrlWithoutPadding(helloWorldDifferentCharsBytes));
+ assertEquals(str, jaxb230Base64.internalEncodeUrlWithoutPadding(bytes));
+ assertEquals(allBytesStr, jaxb230Base64.internalEncodeUrlWithoutPadding(allBytes));
+ }
+}
diff --git a/scribejava-core/src/test/java/com/github/scribejava/core/extractors/BaseStringExtractorTest.java b/scribejava-core/src/test/java/com/github/scribejava/core/extractors/BaseStringExtractorTest.java
new file mode 100644
index 000000000..89865a637
--- /dev/null
+++ b/scribejava-core/src/test/java/com/github/scribejava/core/extractors/BaseStringExtractorTest.java
@@ -0,0 +1,115 @@
+package com.github.scribejava.core.extractors;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Test;
+import com.github.scribejava.core.ObjectMother;
+import com.github.scribejava.core.exceptions.OAuthParametersMissingException;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Verb;
+import static org.junit.Assert.assertThrows;
+import org.junit.function.ThrowingRunnable;
+
+public class BaseStringExtractorTest {
+
+ private BaseStringExtractorImpl extractor;
+ private OAuthRequest request;
+ private OAuthRequest requestPort80;
+ private OAuthRequest requestPort80v2;
+ private OAuthRequest requestPort8080;
+ private OAuthRequest requestPort443;
+ private OAuthRequest requestPort443v2;
+
+ @Before
+ public void setUp() {
+ request = ObjectMother.createSampleOAuthRequest();
+ requestPort80 = ObjectMother.createSampleOAuthRequestPort80();
+ requestPort80v2 = ObjectMother.createSampleOAuthRequestPort80v2();
+ requestPort8080 = ObjectMother.createSampleOAuthRequestPort8080();
+ requestPort443 = ObjectMother.createSampleOAuthRequestPort443();
+ requestPort443v2 = ObjectMother.createSampleOAuthRequestPort443v2();
+ extractor = new BaseStringExtractorImpl();
+ }
+
+ @Test
+ public void shouldExtractBaseStringFromOAuthRequest() {
+ final String expected = "GET&http%3A%2F%2Fexample.com&oauth_callback%3Dhttp%253A%252F%252Fexample%252Fcallback"
+ + "%26oauth_consumer_key%3DAS%2523%2524%255E%252A%2540%2526%26oauth_signature%3DOAuth-Signature"
+ + "%26oauth_timestamp%3D123456";
+ final String baseString = extractor.extract(request);
+ assertEquals(expected, baseString);
+ }
+
+ @Test
+ public void shouldExcludePort80() {
+ final String expected = "GET&http%3A%2F%2Fexample.com&oauth_callback%3Dhttp%253A%252F%252Fexample%252Fcallback"
+ + "%26oauth_consumer_key%3DAS%2523%2524%255E%252A%2540%2526%26oauth_signature%3DOAuth-Signature"
+ + "%26oauth_timestamp%3D123456";
+ final String baseString = extractor.extract(requestPort80);
+ assertEquals(expected, baseString);
+ }
+
+ @Test
+ public void shouldExcludePort80v2() {
+ final String expected = "GET&http%3A%2F%2Fexample.com%2Ftest&oauth_callback%3Dhttp%253A%252F%252Fexample"
+ + "%252Fcallback%26oauth_consumer_key%3DAS%2523%2524%255E%252A%2540%2526%26oauth_signature"
+ + "%3DOAuth-Signature%26oauth_timestamp%3D123456";
+ final String baseString = extractor.extract(requestPort80v2);
+ assertEquals(expected, baseString);
+ }
+
+ @Test
+ public void shouldIncludePort8080() {
+ final String expected = "GET&http%3A%2F%2Fexample.com%3A8080&oauth_callback%3Dhttp%253A%252F%252Fexample"
+ + "%252Fcallback%26oauth_consumer_key%3DAS%2523%2524%255E%252A%2540%2526%26oauth_signature"
+ + "%3DOAuth-Signature%26oauth_timestamp%3D123456";
+ final String baseString = extractor.extract(requestPort8080);
+ assertEquals(expected, baseString);
+ }
+
+ @Test
+ public void shouldExcludePort443() {
+ final String expected = "GET&https%3A%2F%2Fexample.com&oauth_callback%3Dhttp%253A%252F%252Fexample%252Fcallback"
+ + "%26oauth_consumer_key%3DAS%2523%2524%255E%252A%2540%2526%26oauth_signature%3DOAuth-Signature"
+ + "%26oauth_timestamp%3D123456";
+ final String baseString = extractor.extract(requestPort443);
+ assertEquals(expected, baseString);
+ }
+
+ @Test
+ public void shouldExcludePort443v2() {
+ final String expected = "GET&https%3A%2F%2Fexample.com%2Ftest&oauth_callback%3Dhttp%253A%252F%252Fexample"
+ + "%252Fcallback%26oauth_consumer_key%3DAS%2523%2524%255E%252A%2540%2526%26oauth_signature"
+ + "%3DOAuth-Signature%26oauth_timestamp%3D123456";
+ final String baseString = extractor.extract(requestPort443v2);
+ assertEquals(expected, baseString);
+ }
+
+ public void shouldThrowExceptionIfRquestIsNull() {
+ assertThrows(IllegalArgumentException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(null);
+ }
+ });
+ }
+
+ public void shouldThrowExceptionIfRquestHasNoOAuthParameters() {
+ final OAuthRequest request = new OAuthRequest(Verb.GET, "http://example.com");
+ assertThrows(OAuthParametersMissingException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(request);
+ }
+ });
+ }
+
+ @Test
+ public void shouldProperlyEncodeSpaces() {
+ final String expected = "GET&http%3A%2F%2Fexample.com&body%3Dthis%2520param%2520has%2520whitespace"
+ + "%26oauth_callback%3Dhttp%253A%252F%252Fexample%252Fcallback%26oauth_consumer_key"
+ + "%3DAS%2523%2524%255E%252A%2540%2526%26oauth_signature%3DOAuth-Signature%26oauth_timestamp%3D123456";
+ request.addBodyParameter("body", "this param has whitespace");
+ assertEquals(expected, extractor.extract(request));
+ }
+}
diff --git a/scribejava-core/src/test/java/com/github/scribejava/core/extractors/HeaderExtractorTest.java b/scribejava-core/src/test/java/com/github/scribejava/core/extractors/HeaderExtractorTest.java
new file mode 100644
index 000000000..b515908c8
--- /dev/null
+++ b/scribejava-core/src/test/java/com/github/scribejava/core/extractors/HeaderExtractorTest.java
@@ -0,0 +1,66 @@
+package com.github.scribejava.core.extractors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThrows;
+import org.junit.Before;
+import org.junit.Test;
+import com.github.scribejava.core.exceptions.OAuthParametersMissingException;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.ObjectMother;
+import org.junit.function.ThrowingRunnable;
+
+public class HeaderExtractorTest {
+
+ private HeaderExtractorImpl extractor;
+ private OAuthRequest request;
+
+ @Before
+ public void setUp() {
+ request = ObjectMother.createSampleOAuthRequest();
+ extractor = new HeaderExtractorImpl();
+ }
+
+ @Test
+ public void shouldExtractStandardHeader() {
+ final String header = extractor.extract(request);
+ final String oauth = "OAuth ";
+ final String callback = "oauth_callback=\"http%3A%2F%2Fexample%2Fcallback\"";
+ final String signature = "oauth_signature=\"OAuth-Signature\"";
+ final String key = "oauth_consumer_key=\"AS%23%24%5E%2A%40%26\"";
+ final String timestamp = "oauth_timestamp=\"123456\"";
+
+ assertTrue(header.startsWith(oauth));
+ assertTrue(header.contains(callback));
+ assertTrue(header.contains(signature));
+ assertTrue(header.contains(key));
+ assertTrue(header.contains(timestamp));
+ // Assert that header only contains the checked elements above and nothing else
+ assertEquals(", , , ",
+ header.replaceFirst(oauth, "")
+ .replaceFirst(callback, "")
+ .replaceFirst(signature, "")
+ .replaceFirst(key, "")
+ .replaceFirst(timestamp, ""));
+ }
+
+ public void shouldExceptionIfRequestIsNull() {
+ assertThrows(IllegalArgumentException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(null);
+ }
+ });
+ }
+
+ public void shouldExceptionIfRequestHasNoOAuthParams() {
+ final OAuthRequest emptyRequest = new OAuthRequest(Verb.GET, "http://example.com");
+ assertThrows(OAuthParametersMissingException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(emptyRequest);
+ }
+ });
+ }
+}
diff --git a/scribejava-core/src/test/java/com/github/scribejava/core/extractors/OAuth1AccessTokenExtractorTest.java b/scribejava-core/src/test/java/com/github/scribejava/core/extractors/OAuth1AccessTokenExtractorTest.java
new file mode 100644
index 000000000..b62d9bf1e
--- /dev/null
+++ b/scribejava-core/src/test/java/com/github/scribejava/core/extractors/OAuth1AccessTokenExtractorTest.java
@@ -0,0 +1,120 @@
+package com.github.scribejava.core.extractors;
+
+import com.github.scribejava.core.model.Response;
+import org.junit.Before;
+import org.junit.Test;
+import com.github.scribejava.core.exceptions.OAuthException;
+import com.github.scribejava.core.model.OAuth1Token;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import org.junit.function.ThrowingRunnable;
+
+public class OAuth1AccessTokenExtractorTest {
+
+ private OAuth1AccessTokenExtractor extractor;
+
+ @Before
+ public void setUp() {
+ extractor = OAuth1AccessTokenExtractor.instance();
+ }
+
+ @Test
+ public void shouldExtractTokenFromOAuthStandardResponse() throws IOException {
+ final String responseBody = "oauth_token=hh5s93j4hdidpola&oauth_token_secret=hdhd0244k9j7ao03";
+ final OAuth1Token extracted;
+ try (Response response = ok(responseBody)) {
+ extracted = extractor.extract(response);
+ }
+ assertEquals("hh5s93j4hdidpola", extracted.getToken());
+ assertEquals("hdhd0244k9j7ao03", extracted.getTokenSecret());
+ }
+
+ @Test
+ public void shouldExtractTokenFromInvertedOAuthStandardResponse() throws IOException {
+ final String responseBody = "oauth_token_secret=hh5s93j4hdidpola&oauth_token=hdhd0244k9j7ao03";
+ final OAuth1Token extracted;
+ try (Response response = ok(responseBody)) {
+ extracted = extractor.extract(response);
+ }
+ assertEquals("hh5s93j4hdidpola", extracted.getTokenSecret());
+ assertEquals("hdhd0244k9j7ao03", extracted.getToken());
+ }
+
+ @Test
+ public void shouldExtractTokenFromResponseWithCallbackConfirmed() throws IOException {
+ final String responseBody = "oauth_token=hh5s93j4hdidpola&oauth_token_secret=hdhd0244k9j7ao03"
+ + "&callback_confirmed=true";
+ final OAuth1Token extracted;
+ try (Response response = ok(responseBody)) {
+ extracted = extractor.extract(response);
+ }
+ assertEquals("hh5s93j4hdidpola", extracted.getToken());
+ assertEquals("hdhd0244k9j7ao03", extracted.getTokenSecret());
+ }
+
+ @Test
+ public void shouldExtractTokenWithEmptySecret() throws IOException {
+ final String responseBody = "oauth_token=hh5s93j4hdidpola&oauth_token_secret=";
+ final OAuth1Token extracted;
+ try (Response response = ok(responseBody)) {
+ extracted = extractor.extract(response);
+ }
+ assertEquals("hh5s93j4hdidpola", extracted.getToken());
+ assertEquals("", extracted.getTokenSecret());
+ }
+
+ public void shouldThrowExceptionIfTokenIsAbsent() throws IOException {
+ final String responseBody = "oauth_secret=hh5s93j4hdidpola&callback_confirmed=true";
+ try (Response response = ok(responseBody)) {
+ assertThrows(OAuthException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(response);
+ }
+ });
+ }
+ }
+
+ public void shouldThrowExceptionIfSecretIsAbsent() throws IOException {
+ final String responseBody = "oauth_token=hh5s93j4hdidpola&callback_confirmed=true";
+ try (Response response = ok(responseBody)) {
+ assertThrows(OAuthException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(response);
+ }
+ });
+ }
+ }
+
+ public void shouldThrowExceptionIfResponseIsNull() throws IOException {
+ try (Response response = ok(null)) {
+ assertThrows(IllegalArgumentException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(response);
+ }
+ });
+ }
+ }
+
+ public void shouldThrowExceptionIfResponseIsEmptyString() throws IOException {
+ final String responseBody = "";
+ try (Response response = ok(responseBody)) {
+ assertThrows(IllegalArgumentException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(response);
+ }
+ });
+ }
+ }
+
+ private static Response ok(String body) {
+ return new Response(200, /* message */ null, /* headers */ Collections.emptyMap(), body);
+ }
+}
diff --git a/scribejava-core/src/test/java/com/github/scribejava/core/extractors/OAuth2AccessTokenExtractorTest.java b/scribejava-core/src/test/java/com/github/scribejava/core/extractors/OAuth2AccessTokenExtractorTest.java
new file mode 100644
index 000000000..4f71c011d
--- /dev/null
+++ b/scribejava-core/src/test/java/com/github/scribejava/core/extractors/OAuth2AccessTokenExtractorTest.java
@@ -0,0 +1,129 @@
+package com.github.scribejava.core.extractors;
+
+import com.github.scribejava.core.model.Response;
+import org.junit.Before;
+import org.junit.Test;
+import com.github.scribejava.core.exceptions.OAuthException;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import org.junit.function.ThrowingRunnable;
+
+public class OAuth2AccessTokenExtractorTest {
+
+ private OAuth2AccessTokenExtractor extractor;
+
+ @Before
+ public void setUp() {
+ extractor = OAuth2AccessTokenExtractor.instance();
+ }
+
+ @Test
+ public void shouldExtractTokenFromOAuthStandardResponse() throws IOException {
+ final String responseBody = "access_token=166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159"
+ + "|RsXNdKrpxg8L6QNLWcs2TVTmcaE";
+ final OAuth2AccessToken extracted;
+ try (Response response = ok(responseBody)) {
+ extracted = extractor.extract(response);
+ }
+ assertEquals("166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159|RsXNdKrpxg8L6QNLWcs2TVTmcaE",
+ extracted.getAccessToken());
+ }
+
+ @Test
+ public void shouldExtractTokenFromResponseWithExpiresParam() throws IOException {
+ final String responseBody = "access_token=166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159"
+ + "|RsXNdKrpxg8L6QNLWcs2TVTmcaE&expires_in=5108";
+ final OAuth2AccessToken extracted;
+ try (Response response = ok(responseBody)) {
+ extracted = extractor.extract(response);
+ }
+ assertEquals("166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159|RsXNdKrpxg8L6QNLWcs2TVTmcaE",
+ extracted.getAccessToken());
+ assertEquals(Integer.valueOf(5108), extracted.getExpiresIn());
+ }
+
+ @Test
+ public void shouldExtractTokenFromResponseWithExpiresAndRefreshParam() throws IOException {
+ final String responseBody = "access_token=166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159"
+ + "|RsXNdKrpxg8L6QNLWcs2TVTmcaE&expires_in=5108&token_type=bearer&refresh_token=166942940015970";
+ final OAuth2AccessToken extracted;
+ try (Response response = ok(responseBody)) {
+ extracted = extractor.extract(response);
+ }
+ assertEquals("166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159|RsXNdKrpxg8L6QNLWcs2TVTmcaE",
+ extracted.getAccessToken());
+ assertEquals(Integer.valueOf(5108), extracted.getExpiresIn());
+ assertEquals("bearer", extracted.getTokenType());
+ assertEquals("166942940015970", extracted.getRefreshToken());
+ }
+
+ @Test
+ public void shouldExtractTokenFromResponseWithManyParameters() throws IOException {
+ final String responseBody = "access_token=foo1234&other_stuff=yeah_we_have_this_too&number=42";
+ final OAuth2AccessToken extracted;
+ try (Response response = ok(responseBody)) {
+ extracted = extractor.extract(response);
+ }
+ assertEquals("foo1234", extracted.getAccessToken());
+ }
+
+ public void shouldThrowExceptionIfErrorResponse() throws IOException {
+ final String responseBody = "";
+ try (Response response = error(responseBody)) {
+ assertThrows(OAuthException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(response);
+ }
+ });
+ }
+ }
+
+ public void shouldThrowExceptionIfTokenIsAbsent() throws IOException {
+ final String responseBody = "&expires=5108";
+ try (Response response = ok(responseBody)) {
+ assertThrows(OAuthException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(response);
+ }
+ });
+ }
+ }
+
+ public void shouldThrowExceptionIfResponseIsNull() throws IOException {
+ try (Response response = ok(null)) {
+ assertThrows(IllegalArgumentException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(response);
+ }
+ });
+ }
+ }
+
+ public void shouldThrowExceptionIfResponseIsEmptyString() throws IOException {
+ final String responseBody = "";
+ try (Response response = ok(responseBody)) {
+ assertThrows(IllegalArgumentException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ extractor.extract(response);
+ }
+ });
+ }
+ }
+
+ private static Response ok(String body) {
+ return new Response(200, /* message */ null, /* headers */ Collections.emptyMap(), body);
+ }
+
+ private static Response error(String body) {
+ return new Response(400, /* message */ null, /* headers */ Collections.