🎨 OAuth policy supports PKCE; Add an OIDC module that allows you to automatically adapt OIDC applications through IDP's Issuer

This commit is contained in:
yadong.zhang 2021-01-19 14:57:25 +08:00
parent 97bbe53eff
commit 95a8e9ac3c
23 changed files with 720 additions and 69 deletions

View File

@ -10,11 +10,6 @@ package com.fujieid.jap.core;
*/
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 `/`
*/
@ -25,6 +20,11 @@ public class JapConfig {
*/
private String successMessage;
/**
* After logout, redirect to {@code logoutRedirect}. Default is `/`
*/
private String logoutRedirect = "/";
/**
* After failed login, redirect to {@code failureRedirect}. Default is `/error`
*/
@ -40,15 +40,6 @@ public class JapConfig {
*/
private Object options;
public boolean isSession() {
return session;
}
public JapConfig setSession(boolean session) {
this.session = session;
return this;
}
public String getSuccessRedirect() {
return successRedirect;
}
@ -85,6 +76,15 @@ public class JapConfig {
return this;
}
public String getLogoutRedirect() {
return logoutRedirect;
}
public JapConfig setLogoutRedirect(String logoutRedirect) {
this.logoutRedirect = logoutRedirect;
return this;
}
public Object getOptions() {
return options;
}

View File

@ -0,0 +1,83 @@
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);
}
}

View File

@ -0,0 +1,44 @@
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);
}

View File

@ -0,0 +1,57 @@
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);
}
}

View File

@ -2,13 +2,17 @@ package com.fujieid.jap.core.strategy;
import cn.hutool.core.util.ClassUtil;
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.JapSocialException;
import com.fujieid.jap.core.store.JapUserStore;
import com.fujieid.jap.core.store.SessionJapUserStore;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
@ -26,6 +30,10 @@ public abstract class AbstractJapStrategy implements JapStrategy {
* Abstract the user-related function interface, which is implemented by the caller business system.
*/
protected JapUserService japUserService;
/**
* user store
*/
protected JapUserStore japUserStore;
/**
* Jap configuration.
*/
@ -37,8 +45,9 @@ public abstract class AbstractJapStrategy implements JapStrategy {
* @param japUserService japUserService
* @param japConfig japConfig
*/
public AbstractJapStrategy(JapUserService japUserService, JapConfig japConfig) {
public AbstractJapStrategy(JapUserService japUserService, JapUserStore japUserStore, JapConfig japConfig) {
this.japUserService = japUserService;
this.japUserStore = null == japUserStore ? new SessionJapUserStore() : japUserStore;
this.japConfig = japConfig;
}
@ -50,27 +59,20 @@ public abstract class AbstractJapStrategy implements JapStrategy {
* @return boolean
*/
protected boolean checkSession(HttpServletRequest request, HttpServletResponse response) {
if (japConfig.isSession()) {
HttpSession session = request.getSession();
JapUser sessionUser = (JapUser) session.getAttribute(JapConst.SESSION_USER_KEY);
if (null != sessionUser) {
try {
response.sendRedirect(japConfig.getSuccessRedirect());
return true;
} catch (IOException e) {
throw new JapException("JAP failed to redirect via HttpServletResponse.", e);
}
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);
}
}
return false;
}
protected void loginSuccess(JapUser japUser, HttpServletRequest request, HttpServletResponse response) {
if (japConfig.isSession()) {
HttpSession session = request.getSession();
japUser.setPassword(null);
session.setAttribute(JapConst.SESSION_USER_KEY, japUser);
}
japUserStore.save(request, japUser);
try {
response.sendRedirect(japConfig.getSuccessRedirect());
} catch (IOException e) {

View File

@ -19,11 +19,22 @@ public interface JapStrategy {
/**
* This function must be overridden by subclasses. In abstract form, it always throws an exception.
*
* @param config Jap Strategy Configs
* @param request The request to authenticate
* @param config Authenticate Config
* @param request The request to authenticate
* @param response The response to authenticate
*/
default void authenticate(AuthenticateConfig config, HttpServletRequest request, HttpServletResponse response) {
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");
}
}

View File

@ -1,6 +1,7 @@
package com.fujieid.jap.oauth2;
import com.fujieid.jap.core.AuthenticateConfig;
import com.fujieid.jap.oauth2.pkce.PkceCodeChallengeMethod;
/**
* Configuration file of oauth2 module

View File

@ -1,7 +1,5 @@
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.ObjectUtil;
import cn.hutool.core.util.StrUtil;
@ -15,7 +13,12 @@ import com.fujieid.jap.core.JapUserService;
import com.fujieid.jap.core.exception.JapException;
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.store.SessionJapUserStore;
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.Maps;
@ -25,7 +28,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* The OAuth 2.0 authentication strategy authenticates requests using the OAuth 2.0 framework.
@ -42,8 +44,6 @@ import java.util.concurrent.TimeUnit;
*/
public class Oauth2Strategy extends AbstractJapStrategy {
private static TimedCache<String, String> timedCache = CacheUtil.newTimedCache(TimeUnit.MINUTES.toMillis(5));
/**
* `Strategy` constructor.
*
@ -51,9 +51,18 @@ public class Oauth2Strategy extends AbstractJapStrategy {
* @param 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.
@ -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));
JSONObject userinfo = JSONObject.parseObject(userinfoResponse);
if (userinfo.containsKey("error") && StrUtil.isNotBlank(userinfo.getString("error"))) {
@ -102,7 +111,7 @@ public class Oauth2Strategy extends AbstractJapStrategy {
return japUser;
}
private String getAccessToken(HttpServletRequest request, OAuthConfig oAuthConfig) {
protected String getAccessToken(HttpServletRequest request, OAuthConfig oAuthConfig) {
String code = request.getParameter("code");
Map<String, Object> params = Maps.newHashMap();
params.put("grant_type", oAuthConfig.getGrantType());
@ -114,7 +123,7 @@ public class Oauth2Strategy extends AbstractJapStrategy {
}
// pkce 仅适用于授权码模式
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);
JSONObject accessToken = JSONObject.parseObject(tokenResponse);
@ -137,7 +146,7 @@ public class Oauth2Strategy extends AbstractJapStrategy {
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();
params.put("response_type", oAuthConfig.getResponseType());
params.put("client_id", oAuthConfig.getClientId());
@ -152,17 +161,8 @@ public class Oauth2Strategy extends AbstractJapStrategy {
}
// Pkce is only applicable to authorization code mode
if (Oauth2ResponseType.code == oAuthConfig.getResponseType() && oAuthConfig.isEnablePkce()) {
PkceCodeChallengeMethod codeChallengeMethod = Optional.ofNullable(oAuthConfig.getCodeChallengeMethod())
.orElse(PkceCodeChallengeMethod.S256);
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);
}
PkceUtil.addPkceParameters(Optional.ofNullable(oAuthConfig.getCodeChallengeMethod())
.orElse(PkceCodeChallengeMethod.S256), params);
}
String query = URLUtil.buildQuery(params, StandardCharsets.UTF_8);
try {

View File

@ -3,6 +3,7 @@ package com.fujieid.jap.oauth2;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.SecureUtil;
import com.fujieid.jap.oauth2.pkce.PkceCodeChallengeMethod;
import org.jose4j.base64url.Base64Url;
/**

View File

@ -1,4 +1,4 @@
package com.fujieid.jap.oauth2;
package com.fujieid.jap.oauth2.pkce;
/**
* Encryption method of pkce challenge code

View File

@ -0,0 +1,29 @@
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";
}

View File

@ -0,0 +1,54 @@
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);
}
}

View File

@ -1,5 +1,6 @@
package com.fujieid.jap.oauth2;
import com.fujieid.jap.oauth2.pkce.PkceCodeChallengeMethod;
import org.junit.Assert;
import org.junit.Test;

26
jap-oidc/pom.xml Normal file
View File

@ -0,0 +1,26 @@
<?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>

View File

@ -0,0 +1,32 @@
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;
}
}

View File

@ -0,0 +1,99 @@
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;
}
}

View File

@ -0,0 +1,40 @@
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";
}

View File

@ -0,0 +1,81 @@
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");
}
}
}

View File

@ -0,0 +1,47 @@
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));
}
}

View File

@ -0,0 +1,12 @@
/**
* 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;

View File

@ -5,6 +5,8 @@ import com.fujieid.jap.core.JapConfig;
import com.fujieid.jap.core.JapUser;
import com.fujieid.jap.core.JapUserService;
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 javax.servlet.http.HttpServletRequest;
@ -22,12 +24,23 @@ import javax.servlet.http.HttpServletResponse;
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) {
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

View File

@ -4,10 +4,15 @@ 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.*;
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.JapSocialException;
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 me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
@ -16,11 +21,9 @@ import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Map;
@ -43,12 +46,23 @@ public class SocialStrategy extends AbstractJapStrategy {
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) {
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 {
* @param japUserService Required, implement user operations
* @param authStateCache Optional, custom cache implementation class
*/
public SocialStrategy(JapUserService japUserService, JapConfig japConfig, AuthStateCache authStateCache) {
this(japUserService, japConfig);
public SocialStrategy(JapUserService japUserService, JapUserStore japUserStore, JapConfig japConfig, AuthStateCache authStateCache) {
this(japUserService, japUserStore, japConfig);
this.authStateCache = authStateCache;
}
@ -110,13 +124,13 @@ public class SocialStrategy extends AbstractJapStrategy {
* @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) {
AuthResponse<AuthUser> authUserAuthResponse = authRequest.login(authCallback);
AuthResponse<?> authUserAuthResponse = authRequest.login(authCallback);
if (!authUserAuthResponse.ok() || ObjectUtil.isNull(authUserAuthResponse.getData())) {
throw new JapUserException("Third party login of `" + source + "` cannot obtain user information. "
+ authUserAuthResponse.getMsg());
}
AuthUser socialUser = authUserAuthResponse.getData();
AuthUser socialUser = (AuthUser) authUserAuthResponse.getData();
JapUser japUser = japUserService.getByPlatformAndUid(source, socialUser.getUuid());
if (ObjectUtil.isNull(japUser)) {
japUser = japUserService.createAndGetSocialUser(socialUser);

View File

@ -31,6 +31,8 @@
<module>jap-simple</module>
<module>jap-social</module>
<module>jap-oauth2</module>
<module>jap-sso</module>
<module>jap-oidc</module>
</modules>
<properties>
@ -55,6 +57,8 @@
<javax.servlet.version>4.0.1</javax.servlet.version>
<justauth.version>1.15.9</justauth.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>
<dependencies>