diff --git a/pom.xml b/pom.xml
index 44029f5..561c151 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,8 @@
1.2.83
4.17.5.ALL
0.8.2
+ 0.12.3
+ 1.77
@@ -100,6 +102,26 @@
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jwt.version}
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ ${bcpkix-jdk18on.version}
+
diff --git a/src/main/java/me/zhyd/oauth/config/AuthConfig.java b/src/main/java/me/zhyd/oauth/config/AuthConfig.java
index 084649c..fb3b4bd 100644
--- a/src/main/java/me/zhyd/oauth/config/AuthConfig.java
+++ b/src/main/java/me/zhyd/oauth/config/AuthConfig.java
@@ -186,4 +186,16 @@ public class AuthConfig {
* Microsoft Entra ID(原微软 AAD)中的租户 ID
*/
private String tenantId;
+
+ /**
+ * 苹果开发者账号中的密钥标识符
+ * @see create-a-sign-in-with-apple-private-key
+ */
+ private String kid;
+
+ /**
+ * 苹果开发者账号中的团队ID
+ * @see team id
+ */
+ private String teamId;
}
diff --git a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java
index 35b835a..525e008 100644
--- a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java
+++ b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java
@@ -1296,6 +1296,31 @@ public enum AuthDefaultSource implements AuthSource {
public Class extends AuthDefaultRequest> getTargetClass() {
return AuthProginnRequest.class;
}
+ },
+
+ APPLE {
+ @Override
+ public String authorize() {
+ return "https://appleid.apple.com/auth/authorize";
+ }
+
+ /**
+ * @see generate_and_validate_tokens
+ */
+ @Override
+ public String accessToken() {
+ return "https://appleid.apple.com/auth/token";
+ }
+
+ @Override
+ public String userInfo() {
+ return "";
+ }
+
+ @Override
+ public Class extends AuthDefaultRequest> getTargetClass() {
+ return AuthAppleRequest.class;
+ }
}
}
diff --git a/src/main/java/me/zhyd/oauth/enums/AuthResponseStatus.java b/src/main/java/me/zhyd/oauth/enums/AuthResponseStatus.java
index 01e7e08..5cd6c2b 100644
--- a/src/main/java/me/zhyd/oauth/enums/AuthResponseStatus.java
+++ b/src/main/java/me/zhyd/oauth/enums/AuthResponseStatus.java
@@ -29,6 +29,10 @@ public enum AuthResponseStatus {
ILLEGAL_STATUS(5009, "Illegal state"),
REQUIRED_REFRESH_TOKEN(5010, "The refresh token is required; it must not be null"),
ILLEGAL_TOKEN(5011, "Invalid token"),
+ ILLEGAL_KID(5012, "Invalid key identifier(kid)"),
+ ILLEGAL_TEAM_ID(5013, "Invalid team id"),
+ ILLEGAL_CLIENT_ID(5014, "Invalid client id"),
+ ILLEGAL_CLIENT_SECRET(5015, "Invalid client secret"),
;
private final int code;
diff --git a/src/main/java/me/zhyd/oauth/enums/scope/AuthAppleScope.java b/src/main/java/me/zhyd/oauth/enums/scope/AuthAppleScope.java
new file mode 100644
index 0000000..a6e5b44
--- /dev/null
+++ b/src/main/java/me/zhyd/oauth/enums/scope/AuthAppleScope.java
@@ -0,0 +1,19 @@
+package me.zhyd.oauth.enums.scope;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @see scope
+ */
+@Getter
+@AllArgsConstructor
+public enum AuthAppleScope implements AuthScope {
+ EMAIL("email", "用户邮箱", true),
+ NAME("name", "用户名", true),
+ ;
+
+ private final String scope;
+ private final String description;
+ private final boolean isDefault;
+}
diff --git a/src/main/java/me/zhyd/oauth/model/AuthCallback.java b/src/main/java/me/zhyd/oauth/model/AuthCallback.java
index 39c030b..bce57c8 100644
--- a/src/main/java/me/zhyd/oauth/model/AuthCallback.java
+++ b/src/main/java/me/zhyd/oauth/model/AuthCallback.java
@@ -57,4 +57,15 @@ public class AuthCallback implements Serializable {
*/
private String oauth_verifier;
+ /**
+ * 苹果仅在用户首次授权应用程序时返回此值。如果您的应用程序已经获得了用户的授权,那么苹果将不会再次返回此值
+ * @see user info
+ */
+ private String user;
+
+ /**
+ * 苹果错误信息,仅在用户取消授权时返回此值
+ * @see error response
+ */
+ private String error;
}
diff --git a/src/main/java/me/zhyd/oauth/model/AuthToken.java b/src/main/java/me/zhyd/oauth/model/AuthToken.java
index eb4c42b..321da54 100644
--- a/src/main/java/me/zhyd/oauth/model/AuthToken.java
+++ b/src/main/java/me/zhyd/oauth/model/AuthToken.java
@@ -62,4 +62,8 @@ public class AuthToken implements Serializable {
private String screenName;
private Boolean oauthCallbackConfirmed;
+ /**
+ * Apple附带属性
+ */
+ private String username;
}
diff --git a/src/main/java/me/zhyd/oauth/request/AuthAppleRequest.java b/src/main/java/me/zhyd/oauth/request/AuthAppleRequest.java
new file mode 100644
index 0000000..20b41da
--- /dev/null
+++ b/src/main/java/me/zhyd/oauth/request/AuthAppleRequest.java
@@ -0,0 +1,156 @@
+package me.zhyd.oauth.request;
+
+import com.alibaba.fastjson.JSONObject;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.impl.security.AbstractJwk;
+import lombok.Data;
+import me.zhyd.oauth.cache.AuthStateCache;
+import me.zhyd.oauth.config.AuthConfig;
+import me.zhyd.oauth.config.AuthDefaultSource;
+import me.zhyd.oauth.enums.AuthResponseStatus;
+import me.zhyd.oauth.enums.scope.AuthAppleScope;
+import me.zhyd.oauth.exception.AuthException;
+import me.zhyd.oauth.model.AuthCallback;
+import me.zhyd.oauth.model.AuthToken;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.utils.AuthScopeUtils;
+import me.zhyd.oauth.utils.StringUtils;
+import me.zhyd.oauth.utils.UrlBuilder;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.security.PrivateKey;
+import java.util.Base64;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+public class AuthAppleRequest extends AuthDefaultRequest {
+
+ private static final String AUD = "https://appleid.apple.com";
+
+ private volatile PrivateKey privateKey;
+
+ public AuthAppleRequest(AuthConfig config) {
+ super(config, AuthDefaultSource.APPLE);
+ }
+
+ public AuthAppleRequest(AuthConfig config, AuthStateCache authStateCache) {
+ super(config, AuthDefaultSource.APPLE, authStateCache);
+ }
+
+ @Override
+ public String authorize(String state) {
+ return UrlBuilder.fromBaseUrl(super.authorize(state))
+ .queryParam("response_mode", "form_post")
+ .queryParam("scope", this.getScopes(" ", false, AuthScopeUtils.getDefaultScopes(AuthAppleScope.values())))
+ .build();
+ }
+
+ @Override
+ protected AuthToken getAccessToken(AuthCallback authCallback) {
+ if (!StringUtils.isEmpty(authCallback.getError())) {
+ throw new AuthException(authCallback.getError());
+ }
+ this.config.setClientSecret(this.getToken());
+ // if failed will throw AuthException
+ String response = doPostAuthorizationCode(authCallback.getCode());
+ JSONObject accessTokenObject = JSONObject.parseObject(response);
+ // https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse
+ AuthToken.AuthTokenBuilder builder = AuthToken.builder()
+ .accessToken(accessTokenObject.getString("access_token"))
+ .expireIn(accessTokenObject.getIntValue("expires_in"))
+ .refreshToken(accessTokenObject.getString("refresh_token"))
+ .tokenType(accessTokenObject.getString("token_type"))
+ .idToken(accessTokenObject.getString("id_token"));
+ if (!StringUtils.isEmpty(authCallback.getUser())) {
+ try {
+ AppleUserInfo userInfo = JSONObject.parseObject(authCallback.getUser(), AppleUserInfo.class);
+ builder.username(userInfo.getName().getFirstName() + " " + userInfo.getName().getLastName());
+ } catch (Exception ignored) {
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ protected AuthUser getUserInfo(AuthToken authToken) {
+ Base64.Decoder urlDecoder = Base64.getUrlDecoder();
+ String[] idToken = authToken.getIdToken().split("\\.");
+ String payload = new String(urlDecoder.decode(idToken[1]));
+ JSONObject object = JSONObject.parseObject(payload);
+ // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple#3383773
+ return AuthUser.builder()
+ .rawUserInfo(object)
+ .uuid(object.getString("sub"))
+ .email(object.getString("email"))
+ .username(authToken.getUsername())
+ .token(authToken)
+ .source(source.toString())
+ .build();
+ }
+
+ @Override
+ protected void checkConfig(AuthConfig config) {
+ super.checkConfig(config);
+ if (StringUtils.isEmpty(config.getClientId())) {
+ throw new AuthException(AuthResponseStatus.ILLEGAL_CLIENT_ID, source);
+ }
+ if (StringUtils.isEmpty(config.getClientSecret())) {
+ throw new AuthException(AuthResponseStatus.ILLEGAL_CLIENT_SECRET, source);
+ }
+ if (StringUtils.isEmpty(config.getKid())) {
+ throw new AuthException(AuthResponseStatus.ILLEGAL_KID, source);
+ }
+ if (StringUtils.isEmpty(config.getTeamId())) {
+ throw new AuthException(AuthResponseStatus.ILLEGAL_TEAM_ID, source);
+ }
+ }
+
+ /**
+ * 获取token
+ * @see creating-a-client-secret
+ * @return jwt token
+ */
+ private String getToken() {
+ return Jwts.builder().header().add(AbstractJwk.KID.getId(), this.config.getKid()).and()
+ .issuer(this.config.getTeamId())
+ .subject(this.config.getClientId())
+ .audience().add(AUD).and()
+ .expiration(new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(3)))
+ .issuedAt(new Date())
+ .signWith(getPrivateKey())
+ .compact();
+ }
+
+ private PrivateKey getPrivateKey() {
+ if (this.privateKey == null) {
+ synchronized (this) {
+ if (this.privateKey == null) {
+ try (PEMParser pemParser = new PEMParser(new StringReader(this.config.getClientSecret()))) {
+ JcaPEMKeyConverter pemKeyConverter = new JcaPEMKeyConverter();
+ PrivateKeyInfo keyInfo = (PrivateKeyInfo) pemParser.readObject();
+ this.privateKey = pemKeyConverter.getPrivateKey(keyInfo);
+ } catch (IOException e) {
+ throw new AuthException("Failed to get apple private key", e);
+ }
+ }
+ }
+ }
+ return this.privateKey;
+ }
+
+ @Data
+ static class AppleUserInfo {
+ private AppleUsername name;
+ private String email;
+ }
+
+ @Data
+ static class AppleUsername {
+ private String firstName;
+ private String lastName;
+ }
+}
diff --git a/src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java b/src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java
index 0e2e8a4..26384c4 100644
--- a/src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java
+++ b/src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java
@@ -40,7 +40,7 @@ public abstract class AuthDefaultRequest implements AuthRequest {
throw new AuthException(AuthResponseStatus.PARAMETER_INCOMPLETE, source);
}
// 校验配置合法性
- AuthChecker.checkConfig(config, source);
+ this.checkConfig(config);
}
/**
@@ -295,4 +295,8 @@ public abstract class AuthDefaultRequest implements AuthRequest {
return encode ? UrlUtil.urlEncode(scopeStr) : scopeStr;
}
+ protected void checkConfig(AuthConfig config) {
+ AuthChecker.checkConfig(config, source);
+ }
+
}
diff --git a/src/main/java/me/zhyd/oauth/request/AuthFacebookRequest.java b/src/main/java/me/zhyd/oauth/request/AuthFacebookRequest.java
index e9897a8..597acdb 100644
--- a/src/main/java/me/zhyd/oauth/request/AuthFacebookRequest.java
+++ b/src/main/java/me/zhyd/oauth/request/AuthFacebookRequest.java
@@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSONObject;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
+import me.zhyd.oauth.enums.AuthResponseStatus;
import me.zhyd.oauth.enums.AuthUserGender;
import me.zhyd.oauth.enums.scope.AuthFacebookScope;
import me.zhyd.oauth.exception.AuthException;
@@ -11,6 +12,7 @@ import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.utils.AuthScopeUtils;
+import me.zhyd.oauth.utils.GlobalAuthUtils;
import me.zhyd.oauth.utils.UrlBuilder;
/**
@@ -87,6 +89,16 @@ public class AuthFacebookRequest extends AuthDefaultRequest {
.build();
}
+ @Override
+ protected void checkConfig(AuthConfig config) {
+ super.checkConfig(config);
+ // facebook的回调地址必须为https的链接
+ if (AuthDefaultSource.FACEBOOK == source && !GlobalAuthUtils.isHttpsProtocol(config.getRedirectUri())) {
+ // Facebook's redirect uri must use the HTTPS protocol
+ throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
+ }
+ }
+
/**
* 检查响应内容是否正确
*
diff --git a/src/main/java/me/zhyd/oauth/request/AuthMicrosoftCnRequest.java b/src/main/java/me/zhyd/oauth/request/AuthMicrosoftCnRequest.java
index eccd606..73bb054 100644
--- a/src/main/java/me/zhyd/oauth/request/AuthMicrosoftCnRequest.java
+++ b/src/main/java/me/zhyd/oauth/request/AuthMicrosoftCnRequest.java
@@ -3,6 +3,9 @@ package me.zhyd.oauth.request;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
+import me.zhyd.oauth.enums.AuthResponseStatus;
+import me.zhyd.oauth.exception.AuthException;
+import me.zhyd.oauth.utils.GlobalAuthUtils;
/**
* 微软中国登录(世纪华联)
@@ -20,4 +23,14 @@ public class AuthMicrosoftCnRequest extends AbstractAuthMicrosoftRequest {
super(config, AuthDefaultSource.MICROSOFT_CN, authStateCache);
}
+ @Override
+ protected void checkConfig(AuthConfig config) {
+ super.checkConfig(config);
+ // 微软中国的回调地址必须为https的链接或者localhost,不允许使用http
+ if (AuthDefaultSource.MICROSOFT_CN == source && !GlobalAuthUtils.isHttpsProtocolOrLocalHost(config.getRedirectUri())) {
+ // Microsoft's redirect uri must use the HTTPS or localhost
+ throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
+ }
+ }
+
}
diff --git a/src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java b/src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java
index 545f5e2..ac657ee 100644
--- a/src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java
+++ b/src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java
@@ -3,6 +3,9 @@ package me.zhyd.oauth.request;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
+import me.zhyd.oauth.enums.AuthResponseStatus;
+import me.zhyd.oauth.exception.AuthException;
+import me.zhyd.oauth.utils.GlobalAuthUtils;
/**
* 微软登录
@@ -21,4 +24,14 @@ public class AuthMicrosoftRequest extends AbstractAuthMicrosoftRequest {
super(config, AuthDefaultSource.MICROSOFT, authStateCache);
}
+ @Override
+ protected void checkConfig(AuthConfig config) {
+ super.checkConfig(config);
+ // 微软的回调地址必须为https的链接或者localhost,不允许使用http
+ if (AuthDefaultSource.MICROSOFT == source && !GlobalAuthUtils.isHttpsProtocolOrLocalHost(config.getRedirectUri())) {
+ // Microsoft's redirect uri must use the HTTPS or localhost
+ throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
+ }
+ }
+
}
diff --git a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java
index 2737d4c..74c6095 100644
--- a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java
+++ b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java
@@ -63,21 +63,6 @@ public class AuthChecker {
if (!GlobalAuthUtils.isHttpProtocol(redirectUri) && !GlobalAuthUtils.isHttpsProtocol(redirectUri)) {
throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
}
- // facebook的回调地址必须为https的链接
- if (AuthDefaultSource.FACEBOOK == source && !GlobalAuthUtils.isHttpsProtocol(redirectUri)) {
- // Facebook's redirect uri must use the HTTPS protocol
- throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
- }
- // 微软的回调地址必须为https的链接或者localhost,不允许使用http
- if (AuthDefaultSource.MICROSOFT == source && !GlobalAuthUtils.isHttpsProtocolOrLocalHost(redirectUri)) {
- // Microsoft's redirect uri must use the HTTPS or localhost
- throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
- }
- // 微软中国的回调地址必须为https的链接或者localhost,不允许使用http
- if (AuthDefaultSource.MICROSOFT_CN == source && !GlobalAuthUtils.isHttpsProtocolOrLocalHost(redirectUri)) {
- // Microsoft's redirect uri must use the HTTPS or localhost
- throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
- }
}
/**