diff --git a/jap-core/src/main/java/com/fujieid/jap/core/JapUserService.java b/jap-core/src/main/java/com/fujieid/jap/core/JapUserService.java index e459d7c..4ae2c20 100644 --- a/jap-core/src/main/java/com/fujieid/jap/core/JapUserService.java +++ b/jap-core/src/main/java/com/fujieid/jap/core/JapUserService.java @@ -94,12 +94,14 @@ public interface JapUserService { *

* It is suitable for the {@code jap-oauth2} module * - * @param platform oauth2 platform name - * @param userInfo The basic user information returned by the OAuth platform + * @param platform oauth2 platform name + * @param userInfo The basic user information returned by the OAuth platform + * @param tokenInfo The token information returned by the OAuth platform, developers can store tokens + * , type {@code com.fujieid.jap.oauth2.helper.AccessToken} * @return When saving successfully, return {@code JapUser}, otherwise return {@code null} */ - default JapUser createAndGetOauth2User(String platform, Map userInfo) { - throw new JapUserException("JapUserService#createAndGetOauth2User(JSONObject) must be overridden by subclass"); + default JapUser createAndGetOauth2User(String platform, Map userInfo, Object tokenInfo) { + throw new JapUserException("JapUserService#createAndGetOauth2User(String, Map, Object) must be overridden by subclass"); } } diff --git a/jap-core/src/main/java/com/fujieid/jap/core/JapUtil.java b/jap-core/src/main/java/com/fujieid/jap/core/JapUtil.java new file mode 100644 index 0000000..cc58b88 --- /dev/null +++ b/jap-core/src/main/java/com/fujieid/jap/core/JapUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020-2040, 北京符节科技有限公司 (support@fujieid.com & https://www.fujieid.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fujieid.jap.core; + +import cn.hutool.core.util.ObjectUtil; +import com.fujieid.jap.core.exception.JapException; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * The tool class of Jap only provides static methods common to all modules + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @version 1.0.0 + * @date 2021-01-25 21:16 + * @since 1.0.0 + */ +public class JapUtil { + + private static final String REDIRECT_ERROR = "JAP failed to redirect via HttpServletResponse."; + + public static void redirect(String url, HttpServletResponse response) { + redirect(url, REDIRECT_ERROR, response); + } + + public static void redirect(String url, String errorMessage, HttpServletResponse response) { + try { + response.sendRedirect(url); + } catch (IOException ex) { + throw new JapException(errorMessage, ex); + } + } + + public static String convertToStr(Object o) { + if (ObjectUtil.isNull(o)) { + return null; + } + if (o instanceof String) { + return String.valueOf(o); + } + return o.toString(); + } + + public static Integer convertToInt(Object o) { + if (ObjectUtil.isNull(o)) { + return null; + } + if (o instanceof String) { + return Integer.parseInt(String.valueOf(o)); + } + if (o instanceof Integer) { + return (Integer) o; + } + throw new ClassCastException(o + " cannot be converted to Integer type"); + } +} diff --git a/jap-core/src/main/java/com/fujieid/jap/core/strategy/AbstractJapStrategy.java b/jap-core/src/main/java/com/fujieid/jap/core/strategy/AbstractJapStrategy.java index 812b816..a9988ff 100644 --- a/jap-core/src/main/java/com/fujieid/jap/core/strategy/AbstractJapStrategy.java +++ b/jap-core/src/main/java/com/fujieid/jap/core/strategy/AbstractJapStrategy.java @@ -17,11 +17,7 @@ package com.fujieid.jap.core.strategy; import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ObjectUtil; -import com.fujieid.jap.core.AuthenticateConfig; -import com.fujieid.jap.core.JapConfig; -import com.fujieid.jap.core.JapUser; -import com.fujieid.jap.core.JapUserService; -import com.fujieid.jap.core.exception.JapException; +import com.fujieid.jap.core.*; import com.fujieid.jap.core.exception.JapSocialException; import com.fujieid.jap.core.store.JapUserStore; import com.fujieid.jap.core.store.JapUserStoreContextHolder; @@ -31,7 +27,6 @@ import com.fujieid.jap.sso.JapSsoHelper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; /** * General policy handling methods and parameters, policies of other platforms can inherit @@ -97,23 +92,15 @@ public abstract class AbstractJapStrategy implements JapStrategy { protected boolean checkSession(HttpServletRequest request, HttpServletResponse response) { JapUser sessionUser = japUserStore.get(request, response); if (null != sessionUser) { - try { - response.sendRedirect(japConfig.getSuccessRedirect()); - return true; - } catch (IOException e) { - throw new JapException("JAP failed to redirect via HttpServletResponse.", e); - } + JapUtil.redirect(japConfig.getSuccessRedirect(), response); + return true; } return false; } protected void loginSuccess(JapUser japUser, HttpServletRequest request, HttpServletResponse response) { japUserStore.save(request, response, japUser); - try { - response.sendRedirect(japConfig.getSuccessRedirect()); - } catch (IOException e) { - throw new JapException("JAP failed to redirect via HttpServletResponse.", e); - } + JapUtil.redirect(japConfig.getSuccessRedirect(), response); } /** diff --git a/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/OAuthConfig.java b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/OAuthConfig.java index e1a604a..8f1f43c 100644 --- a/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/OAuthConfig.java +++ b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/OAuthConfig.java @@ -101,6 +101,20 @@ public class OAuthConfig extends AuthenticateConfig { */ private PkceCodeChallengeMethod codeChallengeMethod = PkceCodeChallengeMethod.S256; + /** + * The username in `Resource Owner Password Credentials Grant` + * + * @see https://tools.ietf.org/html/rfc6749#section-4.3 + */ + private String username; + + /** + * The password in `Resource Owner Password Credentials Grant` + * + * @see https://tools.ietf.org/html/rfc6749#section-4.3 + */ + private String password; + public String getClientId() { return clientId; } @@ -217,4 +231,22 @@ public class OAuthConfig extends AuthenticateConfig { this.grantType = grantType; return this; } + + public String getUsername() { + return username; + } + + public OAuthConfig setUsername(String username) { + this.username = username; + return this; + } + + public String getPassword() { + return password; + } + + public OAuthConfig setPassword(String password) { + this.password = password; + return this; + } } diff --git a/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/Oauth2Strategy.java b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/Oauth2Strategy.java index cd9326d..74f84ef 100644 --- a/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/Oauth2Strategy.java +++ b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/Oauth2Strategy.java @@ -19,17 +19,14 @@ import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; -import com.fujieid.jap.core.AuthenticateConfig; -import com.fujieid.jap.core.JapConfig; -import com.fujieid.jap.core.JapUser; -import com.fujieid.jap.core.JapUserService; -import com.fujieid.jap.core.exception.JapException; +import com.fujieid.jap.core.*; import com.fujieid.jap.core.exception.JapOauth2Exception; import com.fujieid.jap.core.exception.JapUserException; import com.fujieid.jap.core.store.JapUserStore; import com.fujieid.jap.core.strategy.AbstractJapStrategy; +import com.fujieid.jap.oauth2.helper.AccessToken; +import com.fujieid.jap.oauth2.helper.AccessTokenHelper; import com.fujieid.jap.oauth2.pkce.PkceCodeChallengeMethod; -import com.fujieid.jap.oauth2.pkce.PkceParams; import com.fujieid.jap.oauth2.pkce.PkceUtil; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; @@ -38,7 +35,6 @@ import com.xkcoding.json.JsonUtil; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; @@ -88,7 +84,7 @@ public class Oauth2Strategy extends AbstractJapStrategy { @Override public void authenticate(AuthenticateConfig config, HttpServletRequest request, HttpServletResponse response) { - checkErrorResopnse(request); + Oauth2Util.checkOauthCallbackRequest(request, "Oauth2strategy request failed."); if (this.checkSession(request, response)) { return; @@ -99,11 +95,15 @@ public class Oauth2Strategy extends AbstractJapStrategy { this.checkOauthConfig(oAuthConfig); + boolean isPasswordOrClientMode = oAuthConfig.getGrantType() == Oauth2GrantType.password + || oAuthConfig.getGrantType() == Oauth2GrantType.client_credentials; + // If it is not a callback request, it must be a request to jump to the authorization link - if (!this.isCallback(request)) { + // If it is a password authorization request or a client authorization request, the token will be obtained directly + if (!this.isCallback(request, oAuthConfig) && !isPasswordOrClientMode) { redirectToAuthorizationEndPoint(response, oAuthConfig); } else { - String accessToken = getAccessToken(request, oAuthConfig); + AccessToken accessToken = AccessTokenHelper.getToken(request, oAuthConfig); JapUser japUser = getUserInfo(oAuthConfig, accessToken); this.loginSuccess(japUser, request, response); @@ -111,56 +111,42 @@ public class Oauth2Strategy extends AbstractJapStrategy { } - protected JapUser getUserInfo(OAuthConfig oAuthConfig, String accessToken) { - String userinfoResponse = HttpUtil.post(oAuthConfig.getUserinfoUrl(), ImmutableMap.of("access_token", accessToken), false); + private JapUser getUserInfo(OAuthConfig oAuthConfig, AccessToken accessToken) { + String userinfoResponse = HttpUtil.post(oAuthConfig.getUserinfoUrl(), + ImmutableMap.of("access_token", accessToken.getAccessToken()), false); Map userinfo = JsonUtil.toBean(userinfoResponse, Map.class); - if (userinfo.containsKey("error") && ObjectUtil.isNotEmpty(userinfo.get("error"))) { - throw new JapOauth2Exception("Oauth2Strategy failed to get userinfo with accessToken." + - userinfo.get("error_description") + " " + userinfoResponse); - } - JapUser japUser = this.japUserService.createAndGetOauth2User(oAuthConfig.getPlatform(), userinfo); + + Oauth2Util.checkOauthResponse(userinfoResponse, userinfo, "Oauth2Strategy failed to get userinfo with accessToken."); + + JapUser japUser = this.japUserService.createAndGetOauth2User(oAuthConfig.getPlatform(), userinfo, accessToken); if (ObjectUtil.isNull(japUser)) { throw new JapUserException("Unable to save user information"); } return japUser; } - protected String getAccessToken(HttpServletRequest request, OAuthConfig oAuthConfig) { - String code = request.getParameter("code"); - Map params = Maps.newHashMap(); - params.put("grant_type", oAuthConfig.getGrantType().name()); - params.put("code", code); - params.put("client_id", oAuthConfig.getClientId()); - params.put("client_secret", oAuthConfig.getClientSecret()); - if (StrUtil.isNotBlank(oAuthConfig.getCallbackUrl())) { - params.put("redirect_uri", oAuthConfig.getCallbackUrl()); + private void redirectToAuthorizationEndPoint(HttpServletResponse response, OAuthConfig oAuthConfig) { + String url = null; + // 4.1. Authorization Code Grant https://tools.ietf.org/html/rfc6749#section-4.1 + // 4.2. Implicit Grant https://tools.ietf.org/html/rfc6749#section-4.2 + if (oAuthConfig.getResponseType() == Oauth2ResponseType.code || + oAuthConfig.getResponseType() == Oauth2ResponseType.token) { + url = generateAuthorizationCodeGrantUrl(oAuthConfig); } - // pkce 仅适用于授权码模式 - if (Oauth2ResponseType.code == oAuthConfig.getResponseType() && oAuthConfig.isEnablePkce()) { - params.put(PkceParams.CODE_VERIFIER, PkceUtil.getCacheCodeVerifier()); - } - String tokenResponse = HttpUtil.post(oAuthConfig.getTokenUrl(), params, false); - Map accessToken = JsonUtil.toBean(tokenResponse, Map.class); - if (accessToken.containsKey("error") && ObjectUtil.isNotEmpty(accessToken.get("error"))) { - throw new JapOauth2Exception("Oauth2Strategy failed to get AccessToken." + - accessToken.get("error_description") + " " + tokenResponse); - } - if (!accessToken.containsKey("access_token")) { - throw new JapOauth2Exception("Oauth2Strategy failed to get AccessToken." + tokenResponse); - } - /* - { - "access_token":"2YotnFZFEjr1zCsicMWpAA", - "token_type":"example", - "expires_in":3600, - "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", - "example_parameter":"example_value" - } - */ - return (String) accessToken.get("access_token"); + JapUtil.redirect(url, "JAP failed to redirect to " + oAuthConfig.getAuthorizationUrl() + " through HttpServletResponse.", response); } - protected void redirectToAuthorizationEndPoint(HttpServletResponse response, OAuthConfig oAuthConfig) { + /** + * It is suitable for authorization code mode(rfc6749#4.1) and implicit authorization mode(rfc6749#4.2). + * When it is in authorization code mode, the callback requests return code and state; + * when it is in implicit authorization mode, the callback requests return token related data + * + * @param oAuthConfig oauth config + * @return authorize request url + * @see 4.1. Authorization Code Grant + * @see 4.2. Implicit Grant + */ + private String generateAuthorizationCodeGrantUrl(OAuthConfig oAuthConfig) { Map params = Maps.newHashMap(); params.put("response_type", oAuthConfig.getResponseType()); params.put("client_id", oAuthConfig.getClientId()); @@ -179,49 +165,92 @@ public class Oauth2Strategy extends AbstractJapStrategy { .orElse(PkceCodeChallengeMethod.S256), params); } String query = URLUtil.buildQuery(params, StandardCharsets.UTF_8); - try { - response.sendRedirect(oAuthConfig.getAuthorizationUrl().concat("?").concat(query)); - } catch (IOException ex) { - throw new JapException("JAP failed to redirect to " + oAuthConfig.getAuthorizationUrl() + " through HttpServletResponse.", ex); - } + return oAuthConfig.getAuthorizationUrl().concat("?").concat(query); } + /** + * Check the validity of oauthconfig. + *

+ * 1. For {@code tokenUrl}, this configuration is indispensable for any mode + * 2. When responsetype = code: + * - {@code authorizationUrl} and {@code userinfoUrl} cannot be null + * - {@code clientId} cannot be null + * - {@code clientSecret} cannot be null when PKCE is not enabled + * 3. When responsetype = token: + * - {@code authorizationUrl} and {@code userinfoUrl} cannot be null + * - {@code clientId} cannot be null + * - {@code clientSecret} cannot be null + * 4. When GrantType = password: + * - {@code username} and {@code password} cannot be null + * + * @param oAuthConfig oauth config + */ private void checkOauthConfig(OAuthConfig oAuthConfig) { - if (ObjectUtil.isNull(oAuthConfig.getClientId())) { - throw new JapOauth2Exception("Oauth2Strategy requires a clientId option"); - } - - if (ObjectUtil.isNull(oAuthConfig.getAuthorizationUrl())) { - throw new JapOauth2Exception("Oauth2Strategy requires a authorizationUrl option"); - } - if (ObjectUtil.isNull(oAuthConfig.getTokenUrl())) { - throw new JapOauth2Exception("Oauth2Strategy requires a tokenUrl option"); + throw new JapOauth2Exception("Oauth2Strategy requires a tokenUrl"); } + // For authorization code mode and implicit authorization mode + // refer to: https://tools.ietf.org/html/rfc6749#section-4.1 + // refer to: https://tools.ietf.org/html/rfc6749#section-4.2 + if (oAuthConfig.getResponseType() == Oauth2ResponseType.code || + oAuthConfig.getResponseType() == Oauth2ResponseType.token) { - if (!oAuthConfig.isEnablePkce() && ObjectUtil.isNull(oAuthConfig.getClientSecret())) { - throw new JapOauth2Exception("Oauth2Strategy requires a clientSecret option when PKCE is not enabled."); + if (oAuthConfig.getResponseType() == Oauth2ResponseType.code) { + if (oAuthConfig.getGrantType() != Oauth2GrantType.authorization_code) { + throw new JapOauth2Exception("Invalid grantType `" + oAuthConfig.getGrantType() + "`. " + + "When using authorization code mode, grantType must be `authorization_code`"); + } + + if (!oAuthConfig.isEnablePkce() && ObjectUtil.isNull(oAuthConfig.getClientSecret())) { + throw new JapOauth2Exception("Oauth2Strategy requires a clientSecret when PKCE is not enabled."); + } + } else { + if (ObjectUtil.isNull(oAuthConfig.getClientSecret())) { + throw new JapOauth2Exception("Oauth2Strategy requires a clientSecret"); + } + + } + if (ObjectUtil.isNull(oAuthConfig.getClientId())) { + throw new JapOauth2Exception("Oauth2Strategy requires a clientId"); + } + + if (ObjectUtil.isNull(oAuthConfig.getAuthorizationUrl())) { + throw new JapOauth2Exception("Oauth2Strategy requires a authorizationUrl"); + } + + if (ObjectUtil.isNull(oAuthConfig.getUserinfoUrl())) { + throw new JapOauth2Exception("Oauth2Strategy requires a userinfoUrl"); + } } - } - - private void checkErrorResopnse(HttpServletRequest request) { - String error = request.getParameter("error"); - if (ObjectUtil.isNotNull(error)) { - String errorDescription = request.getParameter("error_description"); - throw new JapOauth2Exception("Oauth2strategy request failed." + errorDescription); + // For password mode + // refer to: https://tools.ietf.org/html/rfc6749#section-4.3 + else { + if (oAuthConfig.getGrantType() == Oauth2GrantType.password) { + if (!ObjectUtil.isAllNotEmpty(oAuthConfig.getUsername(), oAuthConfig.getPassword())) { + throw new JapOauth2Exception("Oauth2Strategy requires username and password in password certificate grant"); + } + } } } /** * Whether it is the callback request after the authorization of the oauth platform is completed, * the judgment basis is as follows: - * - Code is not empty + * - When {@code response_type} is {@code code}, the {@code code} in the request parameter is empty + * - When {@code response_type} is {@code token}, the {@code access_token} in the request parameter is empty * - * @param request callback request + * @param request callback request + * @param oAuthConfig OAuthConfig * @return When true is returned, the current request is a callback request */ - private boolean isCallback(HttpServletRequest request) { - String code = request.getParameter("code"); - return !StrUtil.isEmpty(code); + private boolean isCallback(HttpServletRequest request, OAuthConfig oAuthConfig) { + if (oAuthConfig.getResponseType() == Oauth2ResponseType.code) { + String code = request.getParameter("code"); + return !StrUtil.isEmpty(code); + } else if (oAuthConfig.getResponseType() == Oauth2ResponseType.token) { + String accessToken = request.getParameter("access_token"); + return !StrUtil.isEmpty(accessToken); + } + return false; } } diff --git a/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/Oauth2Util.java b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/Oauth2Util.java index b636217..3c3eb5e 100644 --- a/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/Oauth2Util.java +++ b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/Oauth2Util.java @@ -16,11 +16,17 @@ package com.fujieid.jap.oauth2; import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.crypto.SecureUtil; +import com.fujieid.jap.core.exception.JapOauth2Exception; import com.fujieid.jap.oauth2.pkce.PkceCodeChallengeMethod; import org.jose4j.base64url.Base64Url; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Optional; + /** * OAuth Strategy Util * @@ -62,4 +68,20 @@ public class Oauth2Util { return codeVerifier; } } + + public static void checkOauthResponse(String responseStr, Map responseMap, String errorMsg) { + if (responseMap.containsKey("error") && ObjectUtil.isNotEmpty(responseMap.get("error"))) { + throw new JapOauth2Exception(Optional.ofNullable(errorMsg).orElse("") + + responseMap.get("error_description") + " " + responseStr); + } + } + + public static void checkOauthCallbackRequest(HttpServletRequest request, String errorMsg) { + String error = request.getParameter("error"); + if (ObjectUtil.isNotNull(error)) { + String errorDescription = request.getParameter("error_description"); + throw new JapOauth2Exception(Optional.ofNullable(errorMsg).orElse("") + errorDescription); + } + + } } diff --git a/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/helper/AccessToken.java b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/helper/AccessToken.java new file mode 100644 index 0000000..f0b73ab --- /dev/null +++ b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/helper/AccessToken.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020-2040, 北京符节科技有限公司 (support@fujieid.com & https://www.fujieid.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fujieid.jap.oauth2.helper; + +/** + * AccessToken entity class in OAuth 2.0 authorization process + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @version 1.0.0 + * @date 2021-01-25 22:06 + * @since 1.0.0 + */ +public class AccessToken { + + private String accessToken; + private String tokenType; + private Integer expiresIn; + private String refreshToken; + private String idToken; + private String scope; + + public String getAccessToken() { + return accessToken; + } + + public AccessToken setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public String getTokenType() { + return tokenType; + } + + public AccessToken setTokenType(String tokenType) { + this.tokenType = tokenType; + return this; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public AccessToken setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + return this; + } + + public String getRefreshToken() { + return refreshToken; + } + + public AccessToken setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + public String getIdToken() { + return idToken; + } + + public AccessToken setIdToken(String idToken) { + this.idToken = idToken; + return this; + } + + public String getScope() { + return scope; + } + + public AccessToken setScope(String scope) { + this.scope = scope; + return this; + } +} diff --git a/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/helper/AccessTokenHelper.java b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/helper/AccessTokenHelper.java new file mode 100644 index 0000000..147baaa --- /dev/null +++ b/jap-oauth2/src/main/java/com/fujieid/jap/oauth2/helper/AccessTokenHelper.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2020-2040, 北京符节科技有限公司 (support@fujieid.com & https://www.fujieid.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fujieid.jap.oauth2.helper; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import com.fujieid.jap.core.JapUtil; +import com.fujieid.jap.core.exception.JapOauth2Exception; +import com.fujieid.jap.oauth2.*; +import com.fujieid.jap.oauth2.pkce.PkceParams; +import com.fujieid.jap.oauth2.pkce.PkceUtil; +import com.google.common.collect.Maps; +import com.xkcoding.http.HttpUtil; +import com.xkcoding.json.JsonUtil; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * Access token helper. Provides a unified access token method {@link AccessTokenHelper#getToken(HttpServletRequest, OAuthConfig)} + * for different authorization methods + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @version 1.0.0 + * @date 2021-01-26 10:44 + * @since 1.0.0 + */ +public class AccessTokenHelper { + + /** + * get access_token + * + * @param request Current callback request + * @param oAuthConfig oauth config + * @return AccessToken + */ + public static AccessToken getToken(HttpServletRequest request, OAuthConfig oAuthConfig) { + if (oAuthConfig.getResponseType() == Oauth2ResponseType.code) { + return getAccessTokenOfAuthorizationCodeMode(request, oAuthConfig); + } + if (oAuthConfig.getResponseType() == Oauth2ResponseType.token) { + return getAccessTokenOfImplicitMode(request); + } + if (oAuthConfig.getGrantType() == Oauth2GrantType.password) { + return getAccessTokenOfPasswordMode(request, oAuthConfig); + } + if (oAuthConfig.getGrantType() == Oauth2GrantType.client_credentials) { + return getAccessTokenOfClientMode(request, oAuthConfig); + } + throw new JapOauth2Exception("Oauth2Strategy failed to get AccessToken."); + } + + + /** + * 4.1. Authorization Code Grant + * + * @param request current callback request + * @param oAuthConfig oauth config + * @return token request url + * @see 4.1. Authorization Code Grant + */ + private static AccessToken getAccessTokenOfAuthorizationCodeMode(HttpServletRequest request, OAuthConfig oAuthConfig) { + String code = request.getParameter("code"); + Map params = Maps.newHashMap(); + params.put("grant_type", Oauth2GrantType.authorization_code.name()); + params.put("code", code); + params.put("client_id", oAuthConfig.getClientId()); + params.put("client_secret", oAuthConfig.getClientSecret()); + if (StrUtil.isNotBlank(oAuthConfig.getCallbackUrl())) { + params.put("redirect_uri", oAuthConfig.getCallbackUrl()); + } + // Pkce is only applicable to authorization code mode + if (Oauth2ResponseType.code == oAuthConfig.getResponseType() && oAuthConfig.isEnablePkce()) { + params.put(PkceParams.CODE_VERIFIER, PkceUtil.getCacheCodeVerifier()); + } + + String tokenResponse = HttpUtil.post(oAuthConfig.getTokenUrl(), params, false); + Map tokenMap = JsonUtil.toBean(tokenResponse, Map.class); + Oauth2Util.checkOauthResponse(tokenResponse, tokenMap, "Oauth2Strategy failed to get AccessToken."); + + if (!tokenMap.containsKey("access_token")) { + throw new JapOauth2Exception("Oauth2Strategy failed to get AccessToken." + tokenResponse); + } + + return mapToAccessToken(tokenMap); + } + + /** + * 4.2. Implicit Grant + * + * @param request current callback request + * @return token request url + * @see 4.2. Implicit Grant + */ + private static AccessToken getAccessTokenOfImplicitMode(HttpServletRequest request) { + Oauth2Util.checkOauthCallbackRequest(request, "Oauth2Strategy failed to get AccessToken."); + + if (null == request.getParameter("access_token")) { + throw new JapOauth2Exception("Oauth2Strategy failed to get AccessToken."); + } + + return new AccessToken() + .setAccessToken(request.getParameter("access_token")) + .setRefreshToken(request.getParameter("refresh_token")) + .setIdToken(request.getParameter("id_token")) + .setTokenType(request.getParameter("token_type")) + .setScope(request.getParameter("scope")) + .setExpiresIn(JapUtil.convertToInt(request.getParameter("expires_in"))); + } + + /** + * 4.3. Resource Owner Password Credentials Grant + * + * @param oAuthConfig oauth config + * @return token request url + * @see 4.3. Resource Owner Password Credentials Grant + */ + private static AccessToken getAccessTokenOfPasswordMode(HttpServletRequest request, OAuthConfig oAuthConfig) { + Map params = Maps.newHashMap(); + params.put("grant_type", Oauth2GrantType.password.name()); + params.put("username", oAuthConfig.getUsername()); + params.put("password", oAuthConfig.getPassword()); + if (ArrayUtil.isNotEmpty(oAuthConfig.getScopes())) { + params.put("scope", String.join(Oauth2Const.SCOPE_SEPARATOR, oAuthConfig.getScopes())); + } + String query = URLUtil.buildQuery(params, StandardCharsets.UTF_8); + String url = oAuthConfig.getTokenUrl().concat("?").concat(query); + String tokenResponse = HttpUtil.post(url, params, false); + Map tokenMap = JsonUtil.toBean(tokenResponse, Map.class); + Oauth2Util.checkOauthResponse(tokenResponse, tokenMap, "Oauth2Strategy failed to get AccessToken."); + + if (!tokenMap.containsKey("access_token")) { + throw new JapOauth2Exception("Oauth2Strategy failed to get AccessToken." + tokenResponse); + } + return mapToAccessToken(tokenMap); + } + + /** + * 4.4. Client Credentials Grant + * + * @param oAuthConfig oauth config + * @return token request url + * @see 4.4. Client Credentials Grant + */ + private static AccessToken getAccessTokenOfClientMode(HttpServletRequest request, OAuthConfig oAuthConfig) { + Map params = Maps.newHashMap(); + params.put("grant_type", Oauth2GrantType.client_credentials.name()); + if (ArrayUtil.isNotEmpty(oAuthConfig.getScopes())) { + params.put("scope", String.join(Oauth2Const.SCOPE_SEPARATOR, oAuthConfig.getScopes())); + } + String query = URLUtil.buildQuery(params, StandardCharsets.UTF_8); + String url = oAuthConfig.getTokenUrl().concat("?").concat(query); + + String tokenResponse = HttpUtil.post(url, params, false); + Map tokenMap = JsonUtil.toBean(tokenResponse, Map.class); + Oauth2Util.checkOauthResponse(tokenResponse, tokenMap, "Oauth2Strategy failed to get AccessToken."); + + if (ObjectUtil.isEmpty(request.getParameter("access_token"))) { + throw new JapOauth2Exception("Oauth2Strategy failed to get AccessToken."); + } + + return mapToAccessToken(tokenMap); + } + + private static AccessToken mapToAccessToken(Map tokenMap) { + Object accessToken = tokenMap.get("access_token"); + Object refreshToken = tokenMap.get("refresh_token"); + Object idToken = tokenMap.get("id_token"); + Object tokenType = tokenMap.get("token_type"); + Object expiresIn = tokenMap.get("expires_in"); + Object scope = tokenMap.get("scope"); + return new AccessToken() + .setAccessToken(JapUtil.convertToStr(accessToken)) + .setRefreshToken(JapUtil.convertToStr(refreshToken)) + .setIdToken(JapUtil.convertToStr(idToken)) + .setTokenType(JapUtil.convertToStr(tokenType)) + .setScope(JapUtil.convertToStr(scope)) + .setExpiresIn(JapUtil.convertToInt(expiresIn)); + } +} diff --git a/jap-social/src/main/java/com/fujieid/jap/social/SocialStrategy.java b/jap-social/src/main/java/com/fujieid/jap/social/SocialStrategy.java index 7bd8209..d81cf24 100644 --- a/jap-social/src/main/java/com/fujieid/jap/social/SocialStrategy.java +++ b/jap-social/src/main/java/com/fujieid/jap/social/SocialStrategy.java @@ -19,11 +19,7 @@ import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONObject; -import com.fujieid.jap.core.AuthenticateConfig; -import com.fujieid.jap.core.JapConfig; -import com.fujieid.jap.core.JapUser; -import com.fujieid.jap.core.JapUserService; -import com.fujieid.jap.core.exception.JapException; +import com.fujieid.jap.core.*; import com.fujieid.jap.core.exception.JapSocialException; import com.fujieid.jap.core.exception.JapUserException; import com.fujieid.jap.core.store.JapUserStore; @@ -38,7 +34,6 @@ import me.zhyd.oauth.request.AuthRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.util.Map; /** @@ -117,12 +112,10 @@ public class SocialStrategy extends AbstractJapStrategy { // If it is not a callback request, it must be a request to jump to the authorization link if (!this.isCallback(source, authCallback)) { - try { - response.sendRedirect(authRequest.authorize(socialConfig.getState())); - return; - } catch (IOException ex) { - throw new JapException("JAP failed to redirect to " + source + " authorized endpoint through HttpServletResponse.", ex); - } + String url = authRequest.authorize(socialConfig.getState()); + String redirectErrorMsg = "JAP failed to redirect to " + source + " authorized endpoint through HttpServletResponse."; + JapUtil.redirect(url, redirectErrorMsg, response); + return; } this.login(request, response, source, authRequest, authCallback);