提交 95a8e9ac 编写于 作者: 智布道's avatar 智布道 👁

🎨 OAuth policy supports PKCE; Add an OIDC module that allows you to...

🎨 OAuth policy supports PKCE; Add an OIDC module that allows you to automatically adapt OIDC applications through IDP's Issuer
上级 97bbe53e
...@@ -10,11 +10,6 @@ package com.fujieid.jap.core; ...@@ -10,11 +10,6 @@ package com.fujieid.jap.core;
*/ */
public class JapConfig { public class JapConfig {
/**
* Save login state in session, defaults to {@code true}
*/
private boolean session = true;
/** /**
* After successful login, redirect to {@code successRedirect}. Default is `/` * After successful login, redirect to {@code successRedirect}. Default is `/`
*/ */
...@@ -25,6 +20,11 @@ public class JapConfig { ...@@ -25,6 +20,11 @@ public class JapConfig {
*/ */
private String successMessage; private String successMessage;
/**
* After logout, redirect to {@code logoutRedirect}. Default is `/`
*/
private String logoutRedirect = "/";
/** /**
* After failed login, redirect to {@code failureRedirect}. Default is `/error` * After failed login, redirect to {@code failureRedirect}. Default is `/error`
*/ */
...@@ -40,15 +40,6 @@ public class JapConfig { ...@@ -40,15 +40,6 @@ public class JapConfig {
*/ */
private Object options; private Object options;
public boolean isSession() {
return session;
}
public JapConfig setSession(boolean session) {
this.session = session;
return this;
}
public String getSuccessRedirect() { public String getSuccessRedirect() {
return successRedirect; return successRedirect;
} }
...@@ -85,6 +76,15 @@ public class JapConfig { ...@@ -85,6 +76,15 @@ public class JapConfig {
return this; return this;
} }
public String getLogoutRedirect() {
return logoutRedirect;
}
public JapConfig setLogoutRedirect(String logoutRedirect) {
this.logoutRedirect = logoutRedirect;
return this;
}
public Object getOptions() { public Object getOptions() {
return options; return options;
} }
......
package com.fujieid.jap.core.exception;
/**
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2021/1/18 16:36
* @since 1.0.0
*/
public class OidcException extends JapException {
/**
* Constructs a new runtime exception with {@code null} as its
* detail message. The cause is not initialized, and may subsequently be
* initialized by a call to {@link #initCause}.
*/
public OidcException() {
super();
}
/**
* Constructs a new runtime exception with the specified detail message.
* The cause is not initialized, and may subsequently be initialized by a
* call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public OidcException(String message) {
super(message);
}
/**
* Constructs a new runtime exception with the specified detail message and
* cause. <p>Note that the detail message associated with
* {@code cause} is <i>not</i> automatically incorporated in
* this runtime exception's detail message.
*
* @param message the detail message (which is saved for later retrieval
* by the {@link #getMessage()} method).
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public OidcException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new runtime exception with the specified cause and a
* detail message of <tt>(cause==null ? null : cause.toString())</tt>
* (which typically contains the class and detail message of
* <tt>cause</tt>). This constructor is useful for runtime exceptions
* that are little more than wrappers for other throwables.
*
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public OidcException(Throwable cause) {
super(cause);
}
/**
* Constructs a new runtime exception with the specified detail
* message, cause, suppression enabled or disabled, and writable
* stack trace enabled or disabled.
*
* @param message the detail message.
* @param cause the cause. (A {@code null} value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @param enableSuppression whether or not suppression is enabled
* or disabled
* @param writableStackTrace whether or not the stack trace should
* be writable
* @since 1.7
*/
public OidcException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
package com.fujieid.jap.core.store;
import com.fujieid.jap.core.JapUser;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Save, delete and obtain the login user information.By default, based on local caching,
* developers can use different caching schemes to implement the interface
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2021/1/18 18:50
* @since 1.0.0
*/
public interface JapUserStore {
/**
* Login completed, save user information to the cache
*
* @param request current request
* @param japUser User information after successful login
* @return JapUser
*/
JapUser save(HttpServletRequest request, JapUser japUser);
/**
* Clear user information from cache
*
* @param request current request
*/
void remove(HttpServletRequest request);
/**
* Get the login user information from the cache, return {@code JapUser} if it exists,
* return {@code null} if it is not logged in or the login has expired
*
* @param request current request
* @param response current response
* @return JapUser
*/
JapUser get(HttpServletRequest request, HttpServletResponse response);
}
package com.fujieid.jap.core.store;
import com.fujieid.jap.core.JapConst;
import com.fujieid.jap.core.JapUser;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2021/1/18 19:03
* @since 1.0.0
*/
public class SessionJapUserStore implements JapUserStore {
/**
* Login completed, save user information to the cache
*
* @param request current request
* @param japUser User information after successful login
* @return JapUser
*/
@Override
public JapUser save(HttpServletRequest request, JapUser japUser) {
HttpSession session = request.getSession();
japUser.setPassword(null);
session.setAttribute(JapConst.SESSION_USER_KEY, japUser);
return japUser;
}
/**
* Clear user information from cache
*
* @param request current request
*/
@Override
public void remove(HttpServletRequest request) {
HttpSession session = request.getSession();
session.removeAttribute(JapConst.SESSION_USER_KEY);
}
/**
* Get the login user information from the cache, return {@code JapUser} if it exists,
* return {@code null} if it is not logged in or the login has expired
*
* @param request current request
* @param response current response
* @return JapUser
*/
@Override
public JapUser get(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
return (JapUser) session.getAttribute(JapConst.SESSION_USER_KEY);
}
}
...@@ -2,13 +2,17 @@ package com.fujieid.jap.core.strategy; ...@@ -2,13 +2,17 @@ package com.fujieid.jap.core.strategy;
import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.fujieid.jap.core.*; 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.exception.JapException;
import com.fujieid.jap.core.exception.JapSocialException; import com.fujieid.jap.core.exception.JapSocialException;
import com.fujieid.jap.core.store.JapUserStore;
import com.fujieid.jap.core.store.SessionJapUserStore;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException; import java.io.IOException;
/** /**
...@@ -26,6 +30,10 @@ public abstract class AbstractJapStrategy implements JapStrategy { ...@@ -26,6 +30,10 @@ public abstract class AbstractJapStrategy implements JapStrategy {
* Abstract the user-related function interface, which is implemented by the caller business system. * Abstract the user-related function interface, which is implemented by the caller business system.
*/ */
protected JapUserService japUserService; protected JapUserService japUserService;
/**
* user store
*/
protected JapUserStore japUserStore;
/** /**
* Jap configuration. * Jap configuration.
*/ */
...@@ -37,8 +45,9 @@ public abstract class AbstractJapStrategy implements JapStrategy { ...@@ -37,8 +45,9 @@ public abstract class AbstractJapStrategy implements JapStrategy {
* @param japUserService japUserService * @param japUserService japUserService
* @param japConfig japConfig * @param japConfig japConfig
*/ */
public AbstractJapStrategy(JapUserService japUserService, JapConfig japConfig) { public AbstractJapStrategy(JapUserService japUserService, JapUserStore japUserStore, JapConfig japConfig) {
this.japUserService = japUserService; this.japUserService = japUserService;
this.japUserStore = null == japUserStore ? new SessionJapUserStore() : japUserStore;
this.japConfig = japConfig; this.japConfig = japConfig;
} }
...@@ -50,9 +59,7 @@ public abstract class AbstractJapStrategy implements JapStrategy { ...@@ -50,9 +59,7 @@ public abstract class AbstractJapStrategy implements JapStrategy {
* @return boolean * @return boolean
*/ */
protected boolean checkSession(HttpServletRequest request, HttpServletResponse response) { protected boolean checkSession(HttpServletRequest request, HttpServletResponse response) {
if (japConfig.isSession()) { JapUser sessionUser = japUserStore.get(request, response);
HttpSession session = request.getSession();
JapUser sessionUser = (JapUser) session.getAttribute(JapConst.SESSION_USER_KEY);
if (null != sessionUser) { if (null != sessionUser) {
try { try {
response.sendRedirect(japConfig.getSuccessRedirect()); response.sendRedirect(japConfig.getSuccessRedirect());
...@@ -61,16 +68,11 @@ public abstract class AbstractJapStrategy implements JapStrategy { ...@@ -61,16 +68,11 @@ public abstract class AbstractJapStrategy implements JapStrategy {
throw new JapException("JAP failed to redirect via HttpServletResponse.", e); throw new JapException("JAP failed to redirect via HttpServletResponse.", e);
} }
} }
}
return false; return false;
} }
protected void loginSuccess(JapUser japUser, HttpServletRequest request, HttpServletResponse response) { protected void loginSuccess(JapUser japUser, HttpServletRequest request, HttpServletResponse response) {
if (japConfig.isSession()) { japUserStore.save(request, japUser);
HttpSession session = request.getSession();
japUser.setPassword(null);
session.setAttribute(JapConst.SESSION_USER_KEY, japUser);
}
try { try {
response.sendRedirect(japConfig.getSuccessRedirect()); response.sendRedirect(japConfig.getSuccessRedirect());
} catch (IOException e) { } catch (IOException e) {
......
...@@ -19,11 +19,22 @@ public interface JapStrategy { ...@@ -19,11 +19,22 @@ public interface JapStrategy {
/** /**
* This function must be overridden by subclasses. In abstract form, it always throws an exception. * This function must be overridden by subclasses. In abstract form, it always throws an exception.
* *
* @param config Jap Strategy Configs * @param config Authenticate Config
* @param request The request to authenticate * @param request The request to authenticate
* @param response The response to authenticate * @param response The response to authenticate
*/ */
default void authenticate(AuthenticateConfig config, HttpServletRequest request, HttpServletResponse response) { default void authenticate(AuthenticateConfig config, HttpServletRequest request, HttpServletResponse response) {
throw new JapStrategyException("JapStrategy#authenticate must be overridden by subclass"); throw new JapStrategyException("JapStrategy#authenticate must be overridden by subclass");
} }
/**
* This function must be overridden by subclasses. In abstract form, it always throws an exception.
*
* @param config Authenticate Config
* @param request The request to authenticate
* @param response The response to authenticate
*/
default void logout(AuthenticateConfig config, HttpServletRequest request, HttpServletResponse response) {
throw new JapStrategyException("JapStrategy#logout must be overridden by subclass");
}
} }
package com.fujieid.jap.oauth2; package com.fujieid.jap.oauth2;
import com.fujieid.jap.core.AuthenticateConfig; import com.fujieid.jap.core.AuthenticateConfig;
import com.fujieid.jap.oauth2.pkce.PkceCodeChallengeMethod;
/** /**
* Configuration file of oauth2 module * Configuration file of oauth2 module
......
package com.fujieid.jap.oauth2; package com.fujieid.jap.oauth2;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
...@@ -15,7 +13,12 @@ import com.fujieid.jap.core.JapUserService; ...@@ -15,7 +13,12 @@ import com.fujieid.jap.core.JapUserService;
import com.fujieid.jap.core.exception.JapException; import com.fujieid.jap.core.exception.JapException;
import com.fujieid.jap.core.exception.JapOauth2Exception; import com.fujieid.jap.core.exception.JapOauth2Exception;
import com.fujieid.jap.core.exception.JapUserException; import com.fujieid.jap.core.exception.JapUserException;
import com.fujieid.jap.core.store.JapUserStore;
import com.fujieid.jap.core.store.SessionJapUserStore;
import com.fujieid.jap.core.strategy.AbstractJapStrategy; import com.fujieid.jap.core.strategy.AbstractJapStrategy;
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.ImmutableMap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
...@@ -25,7 +28,6 @@ import java.io.IOException; ...@@ -25,7 +28,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
/** /**
* The OAuth 2.0 authentication strategy authenticates requests using the OAuth 2.0 framework. * The OAuth 2.0 authentication strategy authenticates requests using the OAuth 2.0 framework.
...@@ -42,8 +44,6 @@ import java.util.concurrent.TimeUnit; ...@@ -42,8 +44,6 @@ import java.util.concurrent.TimeUnit;
*/ */
public class Oauth2Strategy extends AbstractJapStrategy { public class Oauth2Strategy extends AbstractJapStrategy {
private static TimedCache<String, String> timedCache = CacheUtil.newTimedCache(TimeUnit.MINUTES.toMillis(5));
/** /**
* `Strategy` constructor. * `Strategy` constructor.
* *
...@@ -51,9 +51,18 @@ public class Oauth2Strategy extends AbstractJapStrategy { ...@@ -51,9 +51,18 @@ public class Oauth2Strategy extends AbstractJapStrategy {
* @param japConfig japConfig * @param japConfig japConfig
*/ */
public Oauth2Strategy(JapUserService japUserService, JapConfig japConfig) { public Oauth2Strategy(JapUserService japUserService, JapConfig japConfig) {
super(japUserService, japConfig); super(japUserService, new SessionJapUserStore(), japConfig);
} }
/**
* `Strategy` constructor.
*
* @param japUserService japUserService
* @param japConfig japConfig
*/
public Oauth2Strategy(JapUserService japUserService, JapUserStore japUserStore, JapConfig japConfig) {
super(japUserService, japUserStore, japConfig);
}
/** /**
* Authenticate request by delegating to a service provider using OAuth 2.0. * Authenticate request by delegating to a service provider using OAuth 2.0.
...@@ -88,7 +97,7 @@ public class Oauth2Strategy extends AbstractJapStrategy { ...@@ -88,7 +97,7 @@ public class Oauth2Strategy extends AbstractJapStrategy {
} }
private JapUser getUserInfo(OAuthConfig oAuthConfig, String accessToken) { protected JapUser getUserInfo(OAuthConfig oAuthConfig, String accessToken) {
String userinfoResponse = HttpUtil.post(oAuthConfig.getUserinfoUrl(), ImmutableMap.of("access_token", accessToken)); String userinfoResponse = HttpUtil.post(oAuthConfig.getUserinfoUrl(), ImmutableMap.of("access_token", accessToken));
JSONObject userinfo = JSONObject.parseObject(userinfoResponse); JSONObject userinfo = JSONObject.parseObject(userinfoResponse);
if (userinfo.containsKey("error") && StrUtil.isNotBlank(userinfo.getString("error"))) { if (userinfo.containsKey("error") && StrUtil.isNotBlank(userinfo.getString("error"))) {
...@@ -102,7 +111,7 @@ public class Oauth2Strategy extends AbstractJapStrategy { ...@@ -102,7 +111,7 @@ public class Oauth2Strategy extends AbstractJapStrategy {
return japUser; return japUser;
} }
private String getAccessToken(HttpServletRequest request, OAuthConfig oAuthConfig) { protected String getAccessToken(HttpServletRequest request, OAuthConfig oAuthConfig) {
String code = request.getParameter("code"); String code = request.getParameter("code");
Map<String, Object> params = Maps.newHashMap(); Map<String, Object> params = Maps.newHashMap();
params.put("grant_type", oAuthConfig.getGrantType()); params.put("grant_type", oAuthConfig.getGrantType());
...@@ -114,7 +123,7 @@ public class Oauth2Strategy extends AbstractJapStrategy { ...@@ -114,7 +123,7 @@ public class Oauth2Strategy extends AbstractJapStrategy {
} }
// pkce 仅适用于授权码模式 // pkce 仅适用于授权码模式
if (Oauth2ResponseType.code == oAuthConfig.getResponseType() && oAuthConfig.isEnablePkce()) { if (Oauth2ResponseType.code == oAuthConfig.getResponseType() && oAuthConfig.isEnablePkce()) {
params.put("code_verifier", timedCache.get("codeVerifier")); params.put(PkceParams.CODE_VERIFIER, PkceUtil.getCacheCodeVerifier());
} }
String tokenResponse = HttpUtil.post(oAuthConfig.getTokenUrl(), params); String tokenResponse = HttpUtil.post(oAuthConfig.getTokenUrl(), params);
JSONObject accessToken = JSONObject.parseObject(tokenResponse); JSONObject accessToken = JSONObject.parseObject(tokenResponse);
...@@ -137,7 +146,7 @@ public class Oauth2Strategy extends AbstractJapStrategy { ...@@ -137,7 +146,7 @@ public class Oauth2Strategy extends AbstractJapStrategy {
return accessToken.getString("access_token"); return accessToken.getString("access_token");
} }
private void redirectToAuthorizationEndPoint(HttpServletResponse response, OAuthConfig oAuthConfig) { protected void redirectToAuthorizationEndPoint(HttpServletResponse response, OAuthConfig oAuthConfig) {
Map<String, Object> params = Maps.newHashMap(); Map<String, Object> params = Maps.newHashMap();
params.put("response_type", oAuthConfig.getResponseType()); params.put("response_type", oAuthConfig.getResponseType());
params.put("client_id", oAuthConfig.getClientId()); params.put("client_id", oAuthConfig.getClientId());
...@@ -152,17 +161,8 @@ public class Oauth2Strategy extends AbstractJapStrategy { ...@@ -152,17 +161,8 @@ public class Oauth2Strategy extends AbstractJapStrategy {
} }
// Pkce is only applicable to authorization code mode // Pkce is only applicable to authorization code mode
if (Oauth2ResponseType.code == oAuthConfig.getResponseType() && oAuthConfig.isEnablePkce()) { if (Oauth2ResponseType.code == oAuthConfig.getResponseType() && oAuthConfig.isEnablePkce()) {
PkceCodeChallengeMethod codeChallengeMethod = Optional.ofNullable(oAuthConfig.getCodeChallengeMethod()) PkceUtil.addPkceParameters(Optional.ofNullable(oAuthConfig.getCodeChallengeMethod())
.orElse(PkceCodeChallengeMethod.S256); .orElse(PkceCodeChallengeMethod.S256), params);
if (PkceCodeChallengeMethod.S256 == oAuthConfig.getCodeChallengeMethod()) {
String codeVerifier = Oauth2Util.generateCodeVerifier();
String codeChallenge = Oauth2Util.generateCodeChallenge(codeChallengeMethod, codeVerifier);
params.put("code_challenge", codeChallenge);
params.put("code_challenge_method", codeChallengeMethod);
// FIXME 需要考虑分布式环境,例如使用 Redis 缓存
timedCache.put("codeVerifier", codeVerifier);
}
} }
String query = URLUtil.buildQuery(params, StandardCharsets.UTF_8); String query = URLUtil.buildQuery(params, StandardCharsets.UTF_8);
try { try {
......
...@@ -3,6 +3,7 @@ package com.fujieid.jap.oauth2; ...@@ -3,6 +3,7 @@ package com.fujieid.jap.oauth2;
import cn.hutool.core.codec.Base64; import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import com.fujieid.jap.oauth2.pkce.PkceCodeChallengeMethod;
import org.jose4j.base64url.Base64Url; import org.jose4j.base64url.Base64Url;
/** /**
......
package com.fujieid.jap.oauth2; package com.fujieid.jap.oauth2.pkce;
/** /**
* Encryption method of pkce challenge code * Encryption method of pkce challenge code
......
package com.fujieid.jap.oauth2.pkce;
/**
* OAuth PKCE Parameters Registry
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2021/1/18 16:49
* @see <a href="https://tools.ietf.org/html/rfc7636#section-6.1" target="_blank">6.1. OAuth Parameters Registry</a>
* @since 1.0.0
*/
public interface PkceParams {
/**
* {@code code_challenge} - used in Authorization Request.
*/
String CODE_CHALLENGE = "code_challenge";
/**
* {@code code_challenge_method} - used in Authorization Request.
*/
String CODE_CHALLENGE_METHOD = "code_challenge_method";
/**
* {@code code_verifier} - used in Token Request.
*/
String CODE_VERIFIER = "code_verifier";
}
package com.fujieid.jap.oauth2.pkce;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import com.fujieid.jap.oauth2.Oauth2Util;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Proof Key for Code Exchange by OAuth Public Client
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2021/1/18 16:52
* @see <a href="https://tools.ietf.org/html/rfc7636" target="_blank">Proof Key for Code Exchange by OAuth Public Clients</a>
* @since 1.0.0
*/
public class PkceUtil {
private static final TimedCache<String, String> timedCache = CacheUtil.newTimedCache(TimeUnit.MINUTES.toMillis(5));
/**
* Create the parameters required by PKCE
*
* @param pkceCodeChallengeMethod After the pkce enhancement protocol is enabled, the generation method of challenge
* code derived from the code verifier sent in the authorization request is `s256` by default
* @param params oauth request params
* @see <a href="https://tools.ietf.org/html/rfc7636#section-1.1" target="_blank">1.1. Protocol Flow</a>
* @see <a href="https://tools.ietf.org/html/rfc7636#section-4.1" target="_blank">4.1. Client Creates a Code Verifier</a>
* @see <a href="https://tools.ietf.org/html/rfc7636#section-4.2" target="_blank">4.2. Client Creates the Code Challenge</a>
* @see <a href="https://tools.ietf.org/html/rfc7636#section-4.3" target="_blank"> Client Sends the Code Challenge with the Authorization Request</a>
*/
public static void addPkceParameters(PkceCodeChallengeMethod pkceCodeChallengeMethod, Map<String, Object> params) {
if (PkceCodeChallengeMethod.S256 == pkceCodeChallengeMethod) {
String codeVerifier = Oauth2Util.generateCodeVerifier();
String codeChallenge = Oauth2Util.generateCodeChallenge(pkceCodeChallengeMethod, codeVerifier);
params.put(PkceParams.CODE_CHALLENGE, codeChallenge);
params.put(PkceParams.CODE_CHALLENGE_METHOD, pkceCodeChallengeMethod);
// FIXME 需要考虑分布式环境,例如使用 Redis 缓存
timedCache.put(PkceParams.CODE_VERIFIER, codeVerifier);
}
}
/**
* Gets the {@code code_verifier} in the cache
*
* @return {@code code_verifier}
*/
public static String getCacheCodeVerifier() {
return timedCache.get(PkceParams.CODE_VERIFIER);
}
}
package com.fujieid.jap.oauth2; package com.fujieid.jap.oauth2;
import com.fujieid.jap.oauth2.pkce.PkceCodeChallengeMethod;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
......
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jap</artifactId>
<groupId>com.fujieid</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jap-oidc</artifactId>
<name>jap-oidc</name>
<description>
OpenID Connect
</description>
<dependencies>
<dependency>
<groupId>com.fujieid</groupId>
<artifactId>jap-oauth2</artifactId>
<version>${jap.version}</version>
</dependency>
</dependencies>
</project>
package com.fujieid.jap.oidc;
import com.fujieid.jap.oauth2.OAuthConfig;
/**
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2021/1/18 16:23
* @since 1.0.0
*/
public class OidcConfig extends OAuthConfig {
private String issuer;
private String userNameAttribute;
public String getIssuer() {
return issuer;
}
public OidcConfig setIssuer(String issuer) {
this.issuer = issuer;
return this;
}
public String getUserNameAttribute() {
return userNameAttribute;
}
public OidcConfig setUserNameAttribute(String userNameAttribute) {
this.userNameAttribute = userNameAttribute;
return this;
}
}
package com.fujieid.jap.oidc;
import java.io.Serializable;
/**
* OpenID Provider Issuer discovery is the process of determining the location of the OpenID Provider.
* <p>
* For the properties defined by this class, please refer to [3. OpenID Provider Metadata]
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2020/10/26 14:47
* @see <a href="https://openid.net/specs/openid-connect-discovery-1_0.html" target="_blank">OpenID Connect Discovery 1.0 incorporating errata set 1</a>
* @see <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata" target="_blank">3. OpenID Provider Metadata</a>
* @since 1.0.0
*/
public class OidcDiscoveryDto implements Serializable {
/**
* Identity provider URL
*/
private String issuer;
/**
* URL of the OP's OAuth 2.0 Authorization Endpoint
*/
private String authorizationEndpoint;
/**
* URL of the OP's OAuth 2.0 Token Endpoint
*/
private String tokenEndpoint;
/**
* URL of the OP's UserInfo Endpoint
*/
private String userinfoEndpoint;
/**
* URL of the OP's Logout Endpoint
*/
private String endSessionEndpoint;
/**
* URL of the OP's JSON Web Key Set [JWK] document
*
* @see <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#JWK" target="_blank">JWK</a>
*/
private String jwksUri;
public String getIssuer() {
return issuer;
}
public OidcDiscoveryDto setIssuer(String issuer) {
this.issuer = issuer;
return this;
}
public String getAuthorizationEndpoint() {
return authorizationEndpoint;
}
public OidcDiscoveryDto setAuthorizationEndpoint(String authorizationEndpoint) {
this.authorizationEndpoint = authorizationEndpoint;
return this;
}
public String getTokenEndpoint() {
return tokenEndpoint;
}
public OidcDiscoveryDto setTokenEndpoint(String tokenEndpoint) {
this.tokenEndpoint = tokenEndpoint;
return this;
}
public String getUserinfoEndpoint() {
return userinfoEndpoint;
}
public OidcDiscoveryDto setUserinfoEndpoint(String userinfoEndpoint) {
this.userinfoEndpoint = userinfoEndpoint;
return this;
}
public String getEndSessionEndpoint() {
return endSessionEndpoint;
}
public OidcDiscoveryDto setEndSessionEndpoint(String endSessionEndpoint) {
this.endSessionEndpoint = endSessionEndpoint;
return this;
}
public String getJwksUri() {
return jwksUri;
}
public OidcDiscoveryDto setJwksUri(String jwksUri) {
this.jwksUri = jwksUri;
return this;
}
}
package com.fujieid.jap.oidc;
/**
* Property name of IDP service discovery
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2021/1/18 17:12
* @see <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata" target="_blank">3. OpenID Provider Metadata</a>
* @since 1.0.0
*/
public interface OidcDiscoveryParams {
/**
* Identity provider URL
*/
String ISSUER = "issuer";
/**
* URL of the OP's OAuth 2.0 Authorization Endpoint
*/
String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
/**
* URL of the OP's OAuth 2.0 Token Endpoint
*/
String TOKEN_ENDPOINT = "token_endpoint";
/**
* URL of the OP's UserInfo Endpoint
*/
String USERINFO_ENDPOINT = "userinfo_endpoint";
/**
* URL of the OP's Logout Endpoint
*/
String END_SESSION_ENDPOINT = "end_session_endpoint";
/**
* URL of the OP's JSON Web Key Set [JWK] document
*
* @see <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#JWK" target="_blank">JWK</a>
*/
String JWKS_URI = "jwks_uri";
}
package com.fujieid.jap.oidc;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fujieid.jap.core.AuthenticateConfig;
import com.fujieid.jap.core.JapConfig;
import com.fujieid.jap.core.JapUserService;
import com.fujieid.jap.core.exception.JapOauth2Exception;
import com.fujieid.jap.core.store.JapUserStore;
import com.fujieid.jap.core.store.SessionJapUserStore;
import com.fujieid.jap.oauth2.OAuthConfig;
import com.fujieid.jap.oauth2.Oauth2Strategy;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol.
* It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server,
* as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2021/1/18 16:27
* @see <a href="https://openid.net/specs/openid-connect-core-1_0.html" target="_blank">OpenID Connect Core 1.0 incorporating errata set 1</a>
* @since 1.0.0
*/
public class OidcStrategy extends Oauth2Strategy {
/**
* `Strategy` constructor.
*
* @param japUserService japUserService
* @param japConfig japConfig
*/
public OidcStrategy(JapUserService japUserService, JapConfig japConfig) {
super(japUserService, new SessionJapUserStore(), japConfig);
}
/**
* `Strategy` constructor.
*
* @param japUserService japUserService
* @param japConfig japConfig
*/
public OidcStrategy(JapUserService japUserService, JapUserStore japUserStore, JapConfig japConfig) {
super(japUserService, japUserStore, japConfig);
}
/**
* Authenticate request by delegating to a service provider using OAuth 2.0.
*
* @param config OAuthConfig
* @param request The request to authenticate
* @param response The response to authenticate
*/
@Override
public void authenticate(AuthenticateConfig config, HttpServletRequest request, HttpServletResponse response) {
this.checkAuthenticateConfig(config, OidcConfig.class);
OidcConfig oidcConfig = (OidcConfig) config;
this.checkOidcConfig(oidcConfig);
String issuer = oidcConfig.getIssuer();
OidcDiscoveryDto discoveryDto = OidcUtil.getOidcDiscovery(issuer);
oidcConfig.setAuthorizationUrl(discoveryDto.getAuthorizationEndpoint())
.setTokenUrl(discoveryDto.getTokenEndpoint())
.setUserinfoUrl(discoveryDto.getUserinfoEndpoint());
OAuthConfig oAuthConfig = BeanUtil.copyProperties(oidcConfig, OAuthConfig.class);
super.authenticate(oAuthConfig, request, response);
}
private void checkOidcConfig(OidcConfig oidcConfig) {
if (ObjectUtil.isNull(oidcConfig.getIssuer())) {
throw new JapOauth2Exception("OidcStrategy requires a issuer option");
}
}
}
package com.fujieid.jap.oidc;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSONObject;
import com.fujieid.jap.core.exception.OidcException;
/**
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @date 2020/12/3 14:13
* @since 1.0.0
*/
public class OidcUtil {
private static final String DISCOVERY_URL = "/.well-known/openid-configuration";
/**
* Get the IDP service configuration
*
* @param issuer IDP identity providers, such as `https://sign.fujieid.com`
* @return OidcDiscoveryDto
*/
public static OidcDiscoveryDto getOidcDiscovery(String issuer) {
if (StrUtil.isBlank(issuer)) {
throw new OidcException("Missing IDP Discovery Url.");
}
String discoveryUrl = issuer.concat(DISCOVERY_URL);
HttpResponse httpResponse = HttpRequest.get(discoveryUrl).execute();
JSONObject jsonObject = JSONObject.parseObject(httpResponse.body());
if (CollectionUtil.isEmpty(jsonObject)) {
throw new OidcException("Unable to parse IDP service discovery configuration information.");
}
return new OidcDiscoveryDto()
.setIssuer(jsonObject.getString(OidcDiscoveryParams.ISSUER))
.setAuthorizationEndpoint(jsonObject.getString(OidcDiscoveryParams.AUTHORIZATION_ENDPOINT))
.setTokenEndpoint(jsonObject.getString(OidcDiscoveryParams.TOKEN_ENDPOINT))
.setUserinfoEndpoint(jsonObject.getString(OidcDiscoveryParams.USERINFO_ENDPOINT))
.setEndSessionEndpoint(jsonObject.getString(OidcDiscoveryParams.END_SESSION_ENDPOINT))
.setJwksUri(jsonObject.getString(OidcDiscoveryParams.JWKS_URI));
}
}
/**
* OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol.
* It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server,
* as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @date 2021/1/18 16:19
* @version 1.0.0
* @see <a href="https://openid.net/specs/openid-connect-core-1_0.html" target="_blank">OpenID Connect Core 1.0 incorporating errata set 1</a>
* @since 1.0.0
*/
package com.fujieid.jap.oidc;
...@@ -5,6 +5,8 @@ import com.fujieid.jap.core.JapConfig; ...@@ -5,6 +5,8 @@ import com.fujieid.jap.core.JapConfig;
import com.fujieid.jap.core.JapUser; import com.fujieid.jap.core.JapUser;
import com.fujieid.jap.core.JapUserService; import com.fujieid.jap.core.JapUserService;
import com.fujieid.jap.core.exception.JapUserException; import com.fujieid.jap.core.exception.JapUserException;
import com.fujieid.jap.core.store.JapUserStore;
import com.fujieid.jap.core.store.SessionJapUserStore;
import com.fujieid.jap.core.strategy.AbstractJapStrategy; import com.fujieid.jap.core.strategy.AbstractJapStrategy;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
...@@ -22,12 +24,23 @@ import javax.servlet.http.HttpServletResponse; ...@@ -22,12 +24,23 @@ import javax.servlet.http.HttpServletResponse;
public class SimpleStrategy extends AbstractJapStrategy { public class SimpleStrategy extends AbstractJapStrategy {
/** /**
* Initialization strategy * `Strategy` constructor.
* *
* @param japUserService Required, implement user operations * @param japUserService japUserService
* @param japConfig japConfig
*/ */
public SimpleStrategy(JapUserService japUserService, JapConfig japConfig) { public SimpleStrategy(JapUserService japUserService, JapConfig japConfig) {
super(japUserService, japConfig); super(japUserService, new SessionJapUserStore(), japConfig);
}
/**
* `Strategy` constructor.
*
* @param japUserService japUserService
* @param japConfig japConfig
*/
public SimpleStrategy(JapUserService japUserService, JapUserStore japUserStore, JapConfig japConfig) {
super(japUserService, japUserStore, japConfig);
} }
@Override @Override
......
...@@ -4,10 +4,15 @@ import cn.hutool.core.collection.CollectionUtil; ...@@ -4,10 +4,15 @@ import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.fujieid.jap.core.*; 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.exception.JapException;
import com.fujieid.jap.core.exception.JapSocialException; import com.fujieid.jap.core.exception.JapSocialException;
import com.fujieid.jap.core.exception.JapUserException; import com.fujieid.jap.core.exception.JapUserException;
import com.fujieid.jap.core.store.JapUserStore;
import com.fujieid.jap.core.store.SessionJapUserStore;
import com.fujieid.jap.core.strategy.AbstractJapStrategy; import com.fujieid.jap.core.strategy.AbstractJapStrategy;
import me.zhyd.oauth.cache.AuthStateCache; import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig; import me.zhyd.oauth.config.AuthConfig;
...@@ -16,11 +21,9 @@ import me.zhyd.oauth.model.AuthCallback; ...@@ -16,11 +21,9 @@ import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse; import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.StringUtils;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
...@@ -43,12 +46,23 @@ public class SocialStrategy extends AbstractJapStrategy { ...@@ -43,12 +46,23 @@ public class SocialStrategy extends AbstractJapStrategy {
private AuthStateCache authStateCache; private AuthStateCache authStateCache;
/** /**
* Initialization strategy * `Strategy` constructor.
* *
* @param japUserService Required, implement user operations * @param japUserService japUserService
* @param japConfig japConfig
*/ */
public SocialStrategy(JapUserService japUserService, JapConfig japConfig) { public SocialStrategy(JapUserService japUserService, JapConfig japConfig) {
super(japUserService, japConfig); super(japUserService, new SessionJapUserStore(), japConfig);
}
/**
* `Strategy` constructor.
*
* @param japUserService japUserService
* @param japConfig japConfig
*/
public SocialStrategy(JapUserService japUserService, JapUserStore japUserStore, JapConfig japConfig) {
super(japUserService, japUserStore, japConfig);
} }
/** /**
...@@ -59,8 +73,8 @@ public class SocialStrategy extends AbstractJapStrategy { ...@@ -59,8 +73,8 @@ public class SocialStrategy extends AbstractJapStrategy {
* @param japUserService Required, implement user operations * @param japUserService Required, implement user operations
* @param authStateCache Optional, custom cache implementation class * @param authStateCache Optional, custom cache implementation class
*/ */
public SocialStrategy(JapUserService japUserService, JapConfig japConfig, AuthStateCache authStateCache) { public SocialStrategy(JapUserService japUserService, JapUserStore japUserStore, JapConfig japConfig, AuthStateCache authStateCache) {
this(japUserService, japConfig); this(japUserService, japUserStore, japConfig);
this.authStateCache = authStateCache; this.authStateCache = authStateCache;
} }
...@@ -110,13 +124,13 @@ public class SocialStrategy extends AbstractJapStrategy { ...@@ -110,13 +124,13 @@ public class SocialStrategy extends AbstractJapStrategy {
* @param authCallback Parse the parameters obtained by the third party callback request * @param authCallback Parse the parameters obtained by the third party callback request
*/ */
private void login(HttpServletRequest request, HttpServletResponse response, String source, AuthRequest authRequest, AuthCallback authCallback) { private void login(HttpServletRequest request, HttpServletResponse response, String source, AuthRequest authRequest, AuthCallback authCallback) {
AuthResponse<AuthUser> authUserAuthResponse = authRequest.login(authCallback); AuthResponse<?> authUserAuthResponse = authRequest.login(authCallback);
if (!authUserAuthResponse.ok() || ObjectUtil.isNull(authUserAuthResponse.getData())) { if (!authUserAuthResponse.ok() || ObjectUtil.isNull(authUserAuthResponse.getData())) {
throw new JapUserException("Third party login of `" + source + "` cannot obtain user information. " throw new JapUserException("Third party login of `" + source + "` cannot obtain user information. "
+ authUserAuthResponse.getMsg()); + authUserAuthResponse.getMsg());
} }
AuthUser socialUser = authUserAuthResponse.getData(); AuthUser socialUser = (AuthUser) authUserAuthResponse.getData();
JapUser japUser = japUserService.getByPlatformAndUid(source, socialUser.getUuid()); JapUser japUser = japUserService.getByPlatformAndUid(source, socialUser.getUuid());
if (ObjectUtil.isNull(japUser)) { if (ObjectUtil.isNull(japUser)) {
japUser = japUserService.createAndGetSocialUser(socialUser); japUser = japUserService.createAndGetSocialUser(socialUser);
......
...@@ -31,6 +31,8 @@ ...@@ -31,6 +31,8 @@
<module>jap-simple</module> <module>jap-simple</module>
<module>jap-social</module> <module>jap-social</module>
<module>jap-oauth2</module> <module>jap-oauth2</module>
<module>jap-sso</module>
<module>jap-oidc</module>
</modules> </modules>
<properties> <properties>
...@@ -55,6 +57,8 @@ ...@@ -55,6 +57,8 @@
<javax.servlet.version>4.0.1</javax.servlet.version> <javax.servlet.version>4.0.1</javax.servlet.version>
<justauth.version>1.15.9</justauth.version> <justauth.version>1.15.9</justauth.version>
<jose4j.version>0.7.1</jose4j.version> <jose4j.version>0.7.1</jose4j.version>
<slf4j-api.version>1.7.30</slf4j-api.version>
<jedis.version>3.2.0</jedis.version>
</properties> </properties>
<dependencies> <dependencies>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册