Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
<fastjson-version>1.2.83</fastjson-version>
<alipay-sdk-version>4.17.5.ALL</alipay-sdk-version>
<jacoco-version>0.8.2</jacoco-version>
<jwt.version>0.12.3</jwt.version>
<bcpkix-jdk18on.version>1.77</bcpkix-jdk18on.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -100,6 +102,26 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bcpkix-jdk18on.version}</version>
</dependency>
</dependencies>

<build>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/me/zhyd/oauth/config/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,16 @@ public String getAuthServerId() {
* Microsoft Entra ID(原微软 AAD)中的租户 ID
*/
private String tenantId;

/**
* 苹果开发者账号中的密钥标识符
* @see <a href="https://developer.apple.com/help/account/configure-app-capabilities/create-a-sign-in-with-apple-private-key/">create-a-sign-in-with-apple-private-key</a>
*/
private String kid;

/**
* 苹果开发者账号中的团队ID
* @see <a href="https://developer.apple.com/help/glossary/team-id/">team id</a>
*/
private String teamId;
}
25 changes: 25 additions & 0 deletions src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,31 @@ public String userInfo() {
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthProginnRequest.class;
}
},

APPLE {
@Override
public String authorize() {
return "https://appleid.apple.com/auth/authorize";
}

/**
* @see <a href="https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens">generate_and_validate_tokens</a>
*/
@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;
}
}

}
4 changes: 4 additions & 0 deletions src/main/java/me/zhyd/oauth/enums/AuthResponseStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/me/zhyd/oauth/enums/scope/AuthAppleScope.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.zhyd.oauth.enums.scope;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* @see <a href="https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230955-scope/">scope</a>
*/
@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;
}
11 changes: 11 additions & 0 deletions src/main/java/me/zhyd/oauth/model/AuthCallback.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,15 @@ public class AuthCallback implements Serializable {
*/
private String oauth_verifier;

/**
* 苹果仅在用户首次授权应用程序时返回此值。如果您的应用程序已经获得了用户的授权,那么苹果将不会再次返回此值
* @see <a href="https://developer.apple.com/documentation/sign_in_with_apple/useri">user info</a>
*/
private String user;

/**
* 苹果错误信息,仅在用户取消授权时返回此值
* @see <a href="https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms">error response</a>
*/
private String error;
}
4 changes: 4 additions & 0 deletions src/main/java/me/zhyd/oauth/model/AuthToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ public class AuthToken implements Serializable {
private String screenName;
private Boolean oauthCallbackConfirmed;

/**
* Apple附带属性
*/
private String username;
}
156 changes: 156 additions & 0 deletions src/main/java/me/zhyd/oauth/request/AuthAppleRequest.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret">creating-a-client-secret</a>
* @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;
}
}
6 changes: 5 additions & 1 deletion src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public AuthDefaultRequest(AuthConfig config, AuthSource source, AuthStateCache a
throw new AuthException(AuthResponseStatus.PARAMETER_INCOMPLETE, source);
}
// 校验配置合法性
AuthChecker.checkConfig(config, source);
this.checkConfig(config);
}

/**
Expand Down Expand Up @@ -295,4 +295,8 @@ protected String getScopes(String separator, boolean encode, List<String> defaul
return encode ? UrlUtil.urlEncode(scopeStr) : scopeStr;
}

protected void checkConfig(AuthConfig config) {
AuthChecker.checkConfig(config, source);
}

}
12 changes: 12 additions & 0 deletions src/main/java/me/zhyd/oauth/request/AuthFacebookRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
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;
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;

/**
Expand Down Expand Up @@ -87,6 +89,16 @@ protected String userInfoUrl(AuthToken authToken) {
.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);
}
}

/**
* 检查响应内容是否正确
*
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/me/zhyd/oauth/request/AuthMicrosoftCnRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
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;

/**
* 微软中国登录(世纪华联)
Expand All @@ -20,4 +23,14 @@ public AuthMicrosoftCnRequest(AuthConfig config, AuthStateCache authStateCache)
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);
}
}

}
13 changes: 13 additions & 0 deletions src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
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;

/**
* 微软登录
Expand All @@ -21,4 +24,14 @@ public AuthMicrosoftRequest(AuthConfig config, AuthStateCache authStateCache) {
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);
}
}

}
Loading