diff --git a/README.md b/README.md
index 7359b47da..3690cb49d 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,7 @@ ScribeJava support out-of-box several HTTP clients:
* XING (https://www.xing.com/)
* Yahoo (https://www.yahoo.com/)
* Misfit (http://misfit.com/)
+* WeChat (http://www.wechat.com/)
* check the [examples folder](https://github.com/scribejava/scribejava/tree/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples)
### Small and modular
diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/WechatApi.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/WechatApi.java
new file mode 100644
index 000000000..551d59f77
--- /dev/null
+++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/WechatApi.java
@@ -0,0 +1,101 @@
+package com.github.scribejava.apis;
+
+import com.github.scribejava.apis.service.WechatService;
+import com.github.scribejava.apis.wechat.WechatOAuth2AccessTokenJsonExtractor;
+import com.github.scribejava.apis.wechat.WechatOAuth2Constants;
+import com.github.scribejava.core.builder.api.DefaultApi20;
+import com.github.scribejava.core.builder.api.OAuth2SignatureType;
+import com.github.scribejava.core.model.OAuthConfig;
+import com.github.scribejava.core.model.ParameterList;
+import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.oauth.OAuth20Service;
+
+import java.util.Arrays;
+import java.util.Map;
+
+public class WechatApi extends DefaultApi20 {
+
+ protected WechatApi() {
+ }
+
+ private static class InstanceHolder {
+ private static final WechatApi INSTANCE = new WechatApi();
+ }
+
+ public static WechatApi instance() {
+ return InstanceHolder.INSTANCE;
+ }
+
+ @Override
+ public String getAccessTokenEndpoint() {
+ return WechatOAuth2Constants.ACCESS_TOKEN_ENDPOINT_URL;
+ }
+
+ @Override
+ public String getRefreshTokenEndpoint() {
+ return WechatOAuth2Constants.REFRESH_TOKEN_ENDPOINT_URL;
+ }
+
+ @Override
+ public WechatOAuth2AccessTokenJsonExtractor getAccessTokenExtractor() {
+ return WechatOAuth2AccessTokenJsonExtractor.instance();
+ }
+
+ @Override
+ public Verb getAccessTokenVerb() {
+ return Verb.GET;
+ }
+
+ /**
+ * @see WeChat
+ * The authorization provided by WeChat does not fully reference the oauth2.0.
+ * For example, the parameter name is not the same, and the parameter order requirements.
+ * so I have to override this method to meet the requirements of it.
+ */
+ @Override
+ public String getAuthorizationUrl(OAuthConfig config, Map additionalParams) {
+ final ParameterList parameters = new ParameterList(additionalParams);
+
+ parameters.add(WechatOAuth2Constants.CLIENT_ID, config.getApiKey());
+
+ final String callback = config.getCallback();
+ if (callback == null) {
+ throw new IllegalArgumentException("Missing required parameter 'redirect_uri'.");
+ }
+ parameters.add(WechatOAuth2Constants.REDIRECT_URI, callback);
+
+ final String responseType = config.getResponseType();
+ if (!WechatOAuth2Constants.CODE_RESPONSE_TYPE.equals(responseType)) {
+ throw new IllegalArgumentException("Parameter response_type must be 'code'.");
+ }
+ parameters.add(WechatOAuth2Constants.RESPONSE_TYPE, config.getResponseType());
+
+ final String scope = config.getScope();
+ if (scope == null || Arrays.binarySearch(WechatOAuth2Constants.SCOPE_VALUES, scope) < 0) {
+ throw new IllegalArgumentException("Parameter scope can only be 'snsapi_base' or 'snsapi_userinfo'.");
+ }
+ parameters.add(WechatOAuth2Constants.SCOPE, scope);
+
+ final String state = config.getState();
+ if (state != null) {
+ parameters.add(WechatOAuth2Constants.STATE, state);
+ }
+
+ return parameters.appendTo(getAuthorizationBaseUrl()).concat("#wechat_redirect");
+ }
+
+ @Override
+ public OAuth20Service createService(OAuthConfig config) {
+ return new WechatService(this, config);
+ }
+
+ @Override
+ public OAuth2SignatureType getSignatureType() {
+ return OAuth2SignatureType.BEARER_URI_QUERY_PARAMETER;
+ }
+
+ @Override
+ protected String getAuthorizationBaseUrl() {
+ return WechatOAuth2Constants.AUTHORIZE_URL;
+ }
+}
diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/service/WechatService.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/service/WechatService.java
new file mode 100644
index 000000000..ac9389180
--- /dev/null
+++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/service/WechatService.java
@@ -0,0 +1,66 @@
+package com.github.scribejava.apis.service;
+
+import com.github.scribejava.apis.wechat.WechatOAuth2Constants;
+import com.github.scribejava.core.builder.api.DefaultApi20;
+import com.github.scribejava.core.model.OAuthConfig;
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.oauth.OAuth20Service;
+
+import java.util.Map;
+
+
+public class WechatService extends OAuth20Service {
+
+ public WechatService(DefaultApi20 api, OAuthConfig config) {
+ super(api, config);
+ }
+
+ /**
+ * The reason to override this method is the same as
+ * {@link com.github.scribejava.apis.WechatApi#getAuthorizationUrl(OAuthConfig, Map)}
+ */
+ @Override
+ protected OAuthRequest createAccessTokenRequest(String code) {
+ final OAuthRequest request = new OAuthRequest(getApi().getAccessTokenVerb(), getApi().getAccessTokenEndpoint());
+ final OAuthConfig config = getConfig();
+
+ request.addParameter(WechatOAuth2Constants.CLIENT_ID, config.getApiKey());
+ request.addParameter(WechatOAuth2Constants.CLIENT_SECRET, config.getApiSecret());
+
+ request.addParameter(WechatOAuth2Constants.CODE, code);
+ request.addParameter(WechatOAuth2Constants.GRANT_TYPE, WechatOAuth2Constants.AUTHORIZATION_CODE);
+
+ return request;
+ }
+
+ /**
+ * The reason to override this method is the same as
+ * {@link com.github.scribejava.apis.WechatApi#getAuthorizationUrl(OAuthConfig, Map)}
+ */
+ @Override
+ protected OAuthRequest createRefreshTokenRequest(String refreshToken) {
+ if (refreshToken == null || refreshToken.isEmpty()) {
+ throw new IllegalArgumentException("The refreshToken cannot be null or empty");
+ }
+ final OAuthRequest request = new OAuthRequest(getApi().getAccessTokenVerb(),
+ getApi().getRefreshTokenEndpoint());
+
+ request.addParameter(WechatOAuth2Constants.CLIENT_ID, getConfig().getApiKey());
+
+ request.addParameter(OAuthConstants.GRANT_TYPE, WechatOAuth2Constants.REFRESH_TOKEN);
+ request.addParameter(WechatOAuth2Constants.REFRESH_TOKEN, refreshToken);
+
+ return request;
+ }
+
+ @Override
+ protected OAuthRequest createAccessTokenPasswordGrantRequest(String username, String password) {
+ throw new UnsupportedOperationException("WeChat doesn't support Resource Owner Password Credentials Grant");
+ }
+
+ @Override
+ protected OAuthRequest createAccessTokenClientCredentialsGrantRequest() {
+ throw new UnsupportedOperationException("WeChat doesn't support Client Credentials Grant");
+ }
+}
diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessToken.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessToken.java
new file mode 100644
index 000000000..750c4759c
--- /dev/null
+++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessToken.java
@@ -0,0 +1,43 @@
+package com.github.scribejava.apis.wechat;
+
+import com.github.scribejava.core.model.OAuth2AccessToken;
+
+public class WechatOAuth2AccessToken extends OAuth2AccessToken {
+
+ private final String openid;
+
+ public WechatOAuth2AccessToken(String accessToken, Integer expiresIn, String refreshToken, String scope,
+ String openid, String response) {
+ super(accessToken, null, expiresIn, refreshToken, scope, response);
+ this.openid = openid;
+ }
+
+ public String getOpenid() {
+ return openid;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof WechatOAuth2AccessToken)) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final WechatOAuth2AccessToken that = (WechatOAuth2AccessToken) o;
+
+ return openid.equals(that.openid);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + openid.hashCode();
+ return result;
+ }
+}
diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenErrorResponse.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenErrorResponse.java
new file mode 100644
index 000000000..244709f30
--- /dev/null
+++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenErrorResponse.java
@@ -0,0 +1,64 @@
+package com.github.scribejava.apis.wechat;
+
+import com.github.scribejava.core.exceptions.OAuthException;
+
+public class WechatOAuth2AccessTokenErrorResponse extends OAuthException {
+
+ private String errcode;
+
+ private String errmsg;
+
+ private String rawResponse;
+
+ public WechatOAuth2AccessTokenErrorResponse(String message) {
+ super(message);
+ }
+
+ public WechatOAuth2AccessTokenErrorResponse(String message, String errcode, String errmsg, String rawResponse) {
+ super(message);
+ this.errcode = errcode;
+ this.errmsg = errmsg;
+ this.rawResponse = rawResponse;
+ }
+
+ public String getErrcode() {
+ return errcode;
+ }
+
+ public String getErrmsg() {
+ return errmsg;
+ }
+
+ public String getRawResponse() {
+ return rawResponse;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final WechatOAuth2AccessTokenErrorResponse that = (WechatOAuth2AccessTokenErrorResponse) o;
+
+ if (errcode != null ? !errcode.equals(that.errcode) : that.errcode != null) {
+ return false;
+ }
+ if (errmsg != null ? !errmsg.equals(that.errmsg) : that.errmsg != null) {
+ return false;
+ }
+ return rawResponse != null ? rawResponse.equals(that.rawResponse) : that.rawResponse == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = errcode != null ? errcode.hashCode() : 0;
+ result = 31 * result + (errmsg != null ? errmsg.hashCode() : 0);
+ result = 31 * result + (rawResponse != null ? rawResponse.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenJsonExtractor.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenJsonExtractor.java
new file mode 100644
index 000000000..05e9419b4
--- /dev/null
+++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenJsonExtractor.java
@@ -0,0 +1,63 @@
+package com.github.scribejava.apis.wechat;
+
+import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.model.Response;
+import com.github.scribejava.core.utils.Preconditions;
+
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+public class WechatOAuth2AccessTokenJsonExtractor extends OAuth2AccessTokenJsonExtractor {
+
+ private static final Pattern ERRCODE_REGEX_PATTERN = Pattern.compile("\"errcode\"\\s*:\\s*([0-9]+)");
+ private static final Pattern ERRMSG_REGEX_PATTERN = Pattern.compile("\"errmsg\"\\s*:\\s*\"([\\S\\s]*?)\"");
+ private static final Pattern OPENID_REGEX_PATTERN = Pattern.compile("\"openid\"\\s*:\\s*\"(\\S*?)\"");
+
+ protected WechatOAuth2AccessTokenJsonExtractor() {
+ }
+
+ private static class InstanceHolder {
+ private static final WechatOAuth2AccessTokenJsonExtractor INSTANCE = new WechatOAuth2AccessTokenJsonExtractor();
+ }
+
+ public static WechatOAuth2AccessTokenJsonExtractor instance() {
+ return InstanceHolder.INSTANCE;
+ }
+
+ @Override
+ public OAuth2AccessToken extract(Response response) throws IOException {
+ checkError(response.getBody());
+ return super.extract(response);
+ }
+
+ /**
+ * Try to extract the error information in response. If 'errcode' or 'errmsg' is found,
+ * {@link WechatOAuth2AccessTokenErrorResponse} will be thrown, otherwise execute super method.
+ */
+ private void checkError(final String response) {
+ if (!Preconditions.hasText(response)) {
+ return;
+ }
+ final String errcode = extractParameter(response, ERRCODE_REGEX_PATTERN, false);
+ final String errmsg = extractParameter(response, ERRMSG_REGEX_PATTERN, false);
+
+ if (errcode != null || errmsg != null) {
+ throw new WechatOAuth2AccessTokenErrorResponse("Obtaining WeChat OAuth2 access_token failed.",
+ errcode, errmsg, response);
+ }
+ }
+
+ @Override
+ public void generateError(String response) {
+ throw new WechatOAuth2AccessTokenErrorResponse("An unknown failure occurred" +
+ " while obtaining the WeChat OAuth2 access_token.");
+ }
+
+ @Override
+ protected WechatOAuth2AccessToken createToken(String accessToken, String tokenType, Integer expiresIn,
+ String refreshToken, String scope, String response) {
+ final String openid = extractParameter(response, OPENID_REGEX_PATTERN, true);
+ return new WechatOAuth2AccessToken(accessToken, expiresIn, refreshToken, scope, openid, response);
+ }
+}
diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2Constants.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2Constants.java
new file mode 100644
index 000000000..b843d47c1
--- /dev/null
+++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2Constants.java
@@ -0,0 +1,20 @@
+package com.github.scribejava.apis.wechat;
+
+import com.github.scribejava.core.model.OAuthConstants;
+
+public interface WechatOAuth2Constants extends OAuthConstants {
+
+ String ACCESS_TOKEN_ENDPOINT_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
+
+ String REFRESH_TOKEN_ENDPOINT_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";
+
+ String AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
+
+ String CODE_RESPONSE_TYPE = "code";
+
+ String[] SCOPE_VALUES = new String[]{"snsapi_base", "snsapi_userinfo "};
+
+ String CLIENT_ID = "appid";
+
+ String CLIENT_SECRET = "secret";
+}
diff --git a/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/WechatExample.java b/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/WechatExample.java
new file mode 100644
index 000000000..c6b5303fe
--- /dev/null
+++ b/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/WechatExample.java
@@ -0,0 +1,86 @@
+package com.github.scribejava.apis.examples;
+
+import com.github.scribejava.apis.WechatApi;
+import com.github.scribejava.apis.wechat.WechatOAuth2AccessToken;
+import com.github.scribejava.core.builder.ServiceBuilder;
+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 java.io.IOException;
+import java.util.Random;
+import java.util.Scanner;
+import java.util.concurrent.ExecutionException;
+
+public class WechatExample {
+
+ private static final String NETWORK_NAME = "WeChat";
+ private static final String PROTECTED_RESOURCE_URL = "https://api.weixin.qq.com/sns/userinfo";
+
+ private WechatExample() {
+ }
+
+ public static void main(String... args) throws IOException, InterruptedException, ExecutionException {
+ // Replace these with your client id and secret
+ final String clientId = "your client id";
+ final String clientSecret = "your client secret";
+ final String secretState = "secret" + new Random().nextInt(999_999);
+ final OAuth20Service service = new ServiceBuilder(clientId)
+ .apiSecret(clientSecret)
+ .scope("snsapi_base")
+ .state(secretState)
+ .callback("http://www.example.com/oauth_callback")
+ .build(WechatApi.instance());
+
+ final Scanner in = new Scanner(System.in, "UTF-8");
+
+ System.out.println("=== " + NETWORK_NAME + "'s OAuth2 Workflow ===");
+ System.out.println();
+
+ // Obtain the Authorization URL
+ System.out.println("Fetching the Authorization URL...");
+ final String authorizationUrl = service.getAuthorizationUrl();
+ System.out.println("Got the Authorization URL!");
+ System.out.println("Now go and authorize ScribeJava here:");
+ System.out.println(authorizationUrl);
+ System.out.println("And paste the authorization code here");
+ System.out.print(">>");
+ final String code = in.nextLine();
+ System.out.println();
+
+ System.out.println("And paste the state from server here. We have set 'secretState'='" + secretState + "'.");
+ System.out.print(">>");
+ final String value = in.nextLine();
+ if (secretState.equals(value)) {
+ System.out.println("State value does match!");
+ } else {
+ System.out.println("Ooops, state value does not match!");
+ System.out.println("Expected = " + secretState);
+ System.out.println("Got = " + value);
+ System.out.println();
+ }
+
+ // Trade the Request Token and Verfier for the Access Token
+ System.out.println("Trading the Request Token for an Access Token...");
+ final WechatOAuth2AccessToken accessToken = (WechatOAuth2AccessToken) service.getAccessToken(code);
+ System.out.println("Got the Access Token!");
+ System.out.println("(The raw response looks like this: " + accessToken.getRawResponse() + "')");
+ System.out.println();
+
+ // Now let's go and ask for a protected resource!
+ System.out.println("Now we're going to access a protected resource...");
+ final OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
+ request.addParameter("openid", accessToken.getOpenid());
+ service.signRequest(accessToken, request);
+ final Response response = service.execute(request);
+ System.out.println("Got it! Lets see what we found...");
+ System.out.println();
+ System.out.println(response.getCode());
+ System.out.println(response.getBody());
+
+ System.out.println();
+ System.out.println("Thats it man! Go and build something awesome with ScribeJava! :)");
+
+ }
+}