diff --git a/io.asgardio.java.oidc.sdk/pom.xml b/io.asgardio.java.oidc.sdk/pom.xml index 97feb5c..e06e08b 100644 --- a/io.asgardio.java.oidc.sdk/pom.xml +++ b/io.asgardio.java.oidc.sdk/pom.xml @@ -91,6 +91,19 @@ org.mock-server mockserver-client-java + + org.powermock + powermock-api-mockito2 + test + + + net.jadler + jadler-core + + + net.jadler + jadler-jetty + diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/OIDCManagerImpl.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/DefaultOIDCManager.java similarity index 79% rename from io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/OIDCManagerImpl.java rename to io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/DefaultOIDCManager.java index dd78612..5348248 100644 --- a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/OIDCManagerImpl.java +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/DefaultOIDCManager.java @@ -18,6 +18,7 @@ package io.asgardio.java.oidc.sdk; +import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.SignedJWT; @@ -40,7 +41,10 @@ import com.nimbusds.oauth2.sdk.id.ClientID; import com.nimbusds.oauth2.sdk.token.AccessToken; import com.nimbusds.oauth2.sdk.token.RefreshToken; -import io.asgardio.java.oidc.sdk.bean.AuthenticationInfo; +import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet; +import io.asgardio.java.oidc.sdk.bean.RequestContext; +import io.asgardio.java.oidc.sdk.bean.SessionContext; import io.asgardio.java.oidc.sdk.bean.User; import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; import io.asgardio.java.oidc.sdk.exception.SSOAgentClientException; @@ -48,6 +52,9 @@ import io.asgardio.java.oidc.sdk.exception.SSOAgentServerException; import io.asgardio.java.oidc.sdk.request.OIDCRequestBuilder; import io.asgardio.java.oidc.sdk.request.OIDCRequestResolver; +import io.asgardio.java.oidc.sdk.request.model.AuthenticationRequest; +import io.asgardio.java.oidc.sdk.request.model.LogoutRequest; +import io.asgardio.java.oidc.sdk.validators.IDTokenValidator; import net.minidev.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.apache.logging.log4j.Level; @@ -66,13 +73,13 @@ /** * OIDC manager implementation. */ -public class OIDCManagerImpl implements OIDCManager { +public class DefaultOIDCManager implements OIDCManager { - private static final Logger logger = LogManager.getLogger(OIDCManagerImpl.class); + private static final Logger logger = LogManager.getLogger(DefaultOIDCManager.class); private OIDCAgentConfig oidcAgentConfig; - public OIDCManagerImpl(OIDCAgentConfig oidcAgentConfig) throws SSOAgentClientException { + public DefaultOIDCManager(OIDCAgentConfig oidcAgentConfig) throws SSOAgentClientException { validateConfig(oidcAgentConfig); this.oidcAgentConfig = oidcAgentConfig; @@ -82,36 +89,38 @@ public OIDCManagerImpl(OIDCAgentConfig oidcAgentConfig) throws SSOAgentClientExc * {@inheritDoc} */ @Override - public void sendForLogin(HttpServletRequest request, HttpServletResponse response, String state) + public RequestContext sendForLogin(HttpServletRequest request, HttpServletResponse response) throws SSOAgentException { OIDCRequestBuilder requestBuilder = new OIDCRequestBuilder(oidcAgentConfig); - String authorizationRequest = requestBuilder.buildAuthorizationRequest(state); + AuthenticationRequest authenticationRequest = requestBuilder.buildAuthenticationRequest(); try { - response.sendRedirect(authorizationRequest); + response.sendRedirect(authenticationRequest.getAuthenticationRequestURI().toString()); } catch (IOException e) { throw new SSOAgentException(e.getMessage(), e); } + return authenticationRequest.getRequestContext(); } /** * {@inheritDoc} */ @Override - public AuthenticationInfo handleOIDCCallback(HttpServletRequest request, HttpServletResponse response) - throws SSOAgentException { + public SessionContext handleOIDCCallback(HttpServletRequest request, HttpServletResponse response, + RequestContext requestContext) throws SSOAgentException { OIDCRequestResolver requestResolver = new OIDCRequestResolver(request, oidcAgentConfig); - AuthenticationInfo authenticationInfo = new AuthenticationInfo(); + SessionContext sessionContext = new SessionContext(); + Nonce nonce = requestContext.getNonce(); try { if (!requestResolver.isError() && requestResolver.isAuthorizationCodeResponse()) { logger.log(Level.TRACE, "Handling the OIDC Authorization response."); - boolean isAuthenticated = handleAuthentication(request, authenticationInfo); + boolean isAuthenticated = handleAuthentication(request, sessionContext, nonce); if (isAuthenticated) { logger.log(Level.TRACE, "Authentication successful. Redirecting to the target page."); - return authenticationInfo; + return sessionContext; } } @@ -127,8 +136,7 @@ public AuthenticationInfo handleOIDCCallback(HttpServletRequest request, HttpSer * {@inheritDoc} */ @Override - public void logout(AuthenticationInfo authenticationInfo, HttpServletResponse response, String state) - throws SSOAgentException { + public RequestContext logout(SessionContext sessionContext, HttpServletResponse response) throws SSOAgentException { if (oidcAgentConfig.getPostLogoutRedirectURI() == null) { logger.info("postLogoutRedirectURI is not configured. Using the callbackURL instead."); @@ -137,17 +145,19 @@ public void logout(AuthenticationInfo authenticationInfo, HttpServletResponse re } OIDCRequestBuilder requestBuilder = new OIDCRequestBuilder(oidcAgentConfig); - String logoutRequest = requestBuilder.buildLogoutRequest(authenticationInfo, state); + LogoutRequest logoutRequest = requestBuilder.buildLogoutRequest(sessionContext); try { - response.sendRedirect(logoutRequest); + response.sendRedirect(logoutRequest.getLogoutRequestURI().toString()); } catch (IOException e) { throw new SSOAgentException(SSOAgentConstants.ErrorMessages.SERVLET_CONNECTION.getMessage(), SSOAgentConstants.ErrorMessages.SERVLET_CONNECTION.getCode(), e); } + return logoutRequest.getRequestContext(); } - private boolean handleAuthentication(final HttpServletRequest request, AuthenticationInfo authenticationInfo) { + private boolean handleAuthentication(final HttpServletRequest request, SessionContext authenticationInfo, + Nonce nonce) throws SSOAgentServerException { AuthorizationResponse authorizationResponse; AuthorizationCode authorizationCode; @@ -173,15 +183,14 @@ private boolean handleAuthentication(final HttpServletRequest request, Authentic return false; } - handleSuccessTokenResponse(tokenResponse, authenticationInfo); + handleSuccessTokenResponse(tokenResponse, authenticationInfo, nonce); return true; } catch (com.nimbusds.oauth2.sdk.ParseException | SSOAgentServerException | IOException e) { - logger.error(e.getMessage(), e); - return false; + throw new SSOAgentServerException(e.getMessage(), e); } } - private void handleSuccessTokenResponse(TokenResponse tokenResponse, AuthenticationInfo authenticationInfo) + private void handleSuccessTokenResponse(TokenResponse tokenResponse, SessionContext sessionContext, Nonce nonce) throws SSOAgentServerException { AccessTokenResponse successResponse = tokenResponse.toSuccessResponse(); @@ -198,13 +207,14 @@ private void handleSuccessTokenResponse(TokenResponse tokenResponse, Authenticat } try { - JWTClaimsSet claimsSet = SignedJWT.parse(idToken).getJWTClaimsSet(); - User user = new User(claimsSet.getSubject(), getUserAttributes(idToken)); - - authenticationInfo.setIdToken(JWTParser.parse(idToken)); - authenticationInfo.setUser(user); - authenticationInfo.setAccessToken(accessToken); - authenticationInfo.setRefreshToken(refreshToken); + JWT idTokenJWT = JWTParser.parse(idToken); + IDTokenValidator idTokenValidator = new IDTokenValidator(oidcAgentConfig, idTokenJWT); + IDTokenClaimsSet claimsSet = idTokenValidator.validate(nonce); + User user = new User(claimsSet.getSubject().getValue(), getUserAttributes(idToken)); + sessionContext.setIdToken(idTokenJWT.getParsedString()); + sessionContext.setUser(user); + sessionContext.setAccessToken(accessToken.toJSONString()); + sessionContext.setRefreshToken(refreshToken.getValue()); } catch (ParseException e) { throw new SSOAgentServerException(SSOAgentConstants.ErrorMessages.ID_TOKEN_PARSE.getMessage(), SSOAgentConstants.ErrorMessages.ID_TOKEN_PARSE.getCode(), e); @@ -297,6 +307,11 @@ private void validateForCode(OIDCAgentConfig oidcAgentConfig) throws SSOAgentCli } if (oidcAgentConfig.getConsumerKey() == null) { + throw new SSOAgentClientException(SSOAgentConstants.ErrorMessages.AGENT_CONFIG_CLIENT_SECRET.getMessage(), + SSOAgentConstants.ErrorMessages.AGENT_CONFIG_CLIENT_SECRET.getCode()); + } + + if (oidcAgentConfig.getConsumerSecret() == null) { throw new SSOAgentClientException(SSOAgentConstants.ErrorMessages.AGENT_CONFIG_CLIENT_ID.getMessage(), SSOAgentConstants.ErrorMessages.AGENT_CONFIG_CLIENT_ID.getCode()); } diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/DefaultOIDCManagerFactory.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/DefaultOIDCManagerFactory.java new file mode 100644 index 0000000..d95b9c1 --- /dev/null +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/DefaultOIDCManagerFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.asgardio.java.oidc.sdk; + +import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; +import io.asgardio.java.oidc.sdk.exception.SSOAgentClientException; + +/** + * A factory to create Default OIDC Manger objects based on a OIDCAgentConfig. + */ +public class DefaultOIDCManagerFactory { + + /** + * Creates a new {@link DefaultOIDCManager} object. + * + * @param oidcAgentConfig The {@link OIDCAgentConfig} object containing the client specific details. + * @return The DefaultOIDCManager instance. + * @throws SSOAgentClientException If the OIDCAgentConfig validation is unsuccessful. + */ + public static OIDCManager createOIDCManager(OIDCAgentConfig oidcAgentConfig) throws SSOAgentClientException { + + return new DefaultOIDCManager(oidcAgentConfig); + } +} diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/HTTPSessionBasedOIDCProcessor.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/HTTPSessionBasedOIDCProcessor.java new file mode 100644 index 0000000..01eb5d3 --- /dev/null +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/HTTPSessionBasedOIDCProcessor.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.asgardio.java.oidc.sdk; + +import io.asgardio.java.oidc.sdk.bean.RequestContext; +import io.asgardio.java.oidc.sdk.bean.SessionContext; +import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; +import io.asgardio.java.oidc.sdk.exception.SSOAgentClientException; +import io.asgardio.java.oidc.sdk.exception.SSOAgentException; +import io.asgardio.java.oidc.sdk.exception.SSOAgentServerException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +/** + * A wrapper class for the {@link DefaultOIDCManager} that provides + * the functionality defined by the {@link OIDCManager} with using + * HTTP sessions as the storage entity for the {@link RequestContext} + * and {@link SessionContext} information. + */ +public class HTTPSessionBasedOIDCProcessor { + + private static final Logger logger = LogManager.getLogger(HTTPSessionBasedOIDCProcessor.class); + + private final OIDCManager defaultOIDCManager; + + public HTTPSessionBasedOIDCProcessor(OIDCAgentConfig oidcAgentConfig) throws SSOAgentClientException { + + defaultOIDCManager = DefaultOIDCManagerFactory.createOIDCManager(oidcAgentConfig); + } + + /** + * Builds an authentication request and redirects. Information + * regarding the authentication session would be retrieved via + * {@link RequestContext} object and then, would be written to + * the http session. + * + * @param request Incoming {@link HttpServletRequest}. + * @param response Outgoing {@link HttpServletResponse}. + * @throws SSOAgentException + */ + public void sendForLogin(HttpServletRequest request, HttpServletResponse response) + throws SSOAgentException { + + HttpSession session = request.getSession(); + RequestContext requestContext = defaultOIDCManager.sendForLogin(request, response); + session.setAttribute(SSOAgentConstants.REQUEST_CONTEXT, requestContext); + } + + /** + * Processes the OIDC callback response and extract the authorization + * code, builds a token request, sends the token request and parse + * the token response where the authenticated user info and tokens + * would be added to the {@link SessionContext} object and written + * into the available http session. + * + * @param request Incoming {@link HttpServletRequest}. + * @param response Outgoing {@link HttpServletResponse}. + * @throws SSOAgentException Upon failed authentication. + */ + public void handleOIDCCallback(HttpServletRequest request, HttpServletResponse response) throws SSOAgentException { + + RequestContext requestContext = getRequestContext(request); + clearSession(request); + SessionContext sessionContext = defaultOIDCManager.handleOIDCCallback(request, response, requestContext); + + if (sessionContext != null) { + clearSession(request); + HttpSession session = request.getSession(); + session.setAttribute(SSOAgentConstants.SESSION_CONTEXT, sessionContext); + } else { + throw new SSOAgentServerException("Null session context."); + } + } + + /** + * Builds a logout request and redirects. + * + * @param request Incoming {@link HttpServletRequest}. + * @param response Outgoing {@link HttpServletResponse} + * @throws SSOAgentException + */ + public void logout(HttpServletRequest request, HttpServletResponse response) throws SSOAgentException { + + SessionContext sessionContext = getSessionContext(request); + clearSession(request); + HttpSession session = request.getSession(); + RequestContext requestContext = defaultOIDCManager.logout(sessionContext, response); + session.setAttribute(SSOAgentConstants.REQUEST_CONTEXT, requestContext); + } + + private void clearSession(HttpServletRequest request) { + + HttpSession session = request.getSession(false); + + if (session != null) { + session.invalidate(); + } + } + + private RequestContext getRequestContext(HttpServletRequest request) throws SSOAgentServerException { + + HttpSession session = request.getSession(false); + + if (session != null && session.getAttribute(SSOAgentConstants.REQUEST_CONTEXT) != null) { + return (RequestContext) request.getSession(false) + .getAttribute(SSOAgentConstants.REQUEST_CONTEXT); + } + throw new SSOAgentServerException("Request context null."); + } + + private SessionContext getSessionContext(HttpServletRequest request) throws SSOAgentServerException { + + HttpSession session = request.getSession(false); + + if (session != null && session.getAttribute(SSOAgentConstants.SESSION_CONTEXT) != null) { + return (SessionContext) request.getSession(false) + .getAttribute(SSOAgentConstants.SESSION_CONTEXT); + } + throw new SSOAgentServerException("Session context null."); + } +} diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/OIDCManager.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/OIDCManager.java index dfe9a2a..b6b9d97 100644 --- a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/OIDCManager.java +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/OIDCManager.java @@ -18,9 +18,11 @@ package io.asgardio.java.oidc.sdk; -import io.asgardio.java.oidc.sdk.bean.AuthenticationInfo; +import io.asgardio.java.oidc.sdk.bean.RequestContext; +import io.asgardio.java.oidc.sdk.bean.SessionContext; import io.asgardio.java.oidc.sdk.bean.User; import io.asgardio.java.oidc.sdk.exception.SSOAgentException; +import io.asgardio.java.oidc.sdk.request.model.AuthenticationRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -35,33 +37,36 @@ public interface OIDCManager { * * @param request Incoming {@link HttpServletRequest}. * @param response Outgoing {@link HttpServletResponse} - * @param state State parameter for the session. + * @return {@link RequestContext} Object containing details regarding the state ID, nonce value for the + * {@link AuthenticationRequest}. * @throws SSOAgentException */ - void sendForLogin(HttpServletRequest request, HttpServletResponse response, String state) throws SSOAgentException; + RequestContext sendForLogin(HttpServletRequest request, HttpServletResponse response) + throws SSOAgentException; /** * Processes the OIDC callback response and extract the authorization code, builds a token request, sends the * token request and parse the token response where the authenticated user info and tokens would be added to the - * {@link AuthenticationInfo} object and returned. + * {@link SessionContext} object and returned. * - * @param request Incoming {@link HttpServletRequest}. - * @param response Outgoing {@link HttpServletResponse} - * @return {@link AuthenticationInfo} Object containing the authenticated {@link User}, AccessToken, RefreshToken + * @param request Incoming {@link HttpServletRequest}. + * @param response Outgoing {@link HttpServletResponse}. + * @param requestContext {@link RequestContext} object containing the authentication request related information. + * @return {@link SessionContext} Object containing the authenticated {@link User}, AccessToken, RefreshToken * and IDToken. * @throws SSOAgentException Upon failed authentication. */ - AuthenticationInfo handleOIDCCallback(HttpServletRequest request, HttpServletResponse response) - throws SSOAgentException; + SessionContext handleOIDCCallback(HttpServletRequest request, HttpServletResponse response, + RequestContext requestContext) throws SSOAgentException; /** * Builds a logout request and redirects. * - * @param authenticationInfo {@link AuthenticationInfo} of the logged in session. - * @param response Outgoing {@link HttpServletResponse} - * @param state State parameter for the session. + * @param sessionContext {@link SessionContext} of the logged in session. + * @param response Outgoing {@link HttpServletResponse} + * @return {@link RequestContext} Object containing details regarding the state ID and other parameters of the + * {@link io.asgardio.java.oidc.sdk.request.model.LogoutRequest}. * @throws SSOAgentException */ - void logout(AuthenticationInfo authenticationInfo, HttpServletResponse response, String state) - throws SSOAgentException; + RequestContext logout(SessionContext sessionContext, HttpServletResponse response) throws SSOAgentException; } diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/SSOAgentConstants.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/SSOAgentConstants.java index 22bb0f9..5713d16 100644 --- a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/SSOAgentConstants.java +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/SSOAgentConstants.java @@ -35,6 +35,8 @@ public class SSOAgentConstants { public static final String ID_TOKEN = "id_token"; public static final String SESSION_STATE = "session_state"; public static final String USER = "user"; + public static final String REQUEST_CONTEXT = "request_context"; + public static final String SESSION_CONTEXT = "session_context"; // Keystore file properties. public static final String KEYSTORE_NAME = "keystorename"; @@ -46,6 +48,7 @@ public class SSOAgentConstants { public static final String CALL_BACK_URL = "callBackURL"; public static final String SKIP_URIS = "skipURIs"; public static final String INDEX_PAGE = "indexPage"; + public static final String ERROR_PAGE = "errorPage"; public static final String LOGOUT_URL = "logoutURL"; public static final String SCOPE = "scope"; public static final String OAUTH2_GRANT_TYPE = "grantType"; @@ -57,8 +60,12 @@ public class SSOAgentConstants { public static final String OIDC_JWKS_ENDPOINT = "jwksEndpoint"; public static final String POST_LOGOUT_REDIRECTION_URI = "postLogoutRedirectURI"; public static final String AUTHENTICATED = "authenticated"; - public static final String AUTHENTICATION_INFO = "authenticationInfo"; public static final String OIDC_OPENID = "openid"; + public static final String AZP = "azp"; + public static final String TRUSTED_AUDIENCE = "trustedAudience"; + public static final String ID_TOKEN_SIGN_ALG = "signatureAlgorithm"; + public static final String NONCE = "nonce"; + public static final String AGENT_EXCEPTION = "AgentException"; // Request headers. public static final String REFERER = "referer"; @@ -85,7 +92,8 @@ public enum ErrorMessages { AGENT_CONFIG_CLIENT_ID("18006", "Consumer Key/Client ID must not be null. This refers to the client identifier assigned to the " + "Relying Party during its registration with the OpenID Provider."), - AGENT_CONFIG_CALLBACK_URL("18007", + AGENT_CONFIG_CLIENT_SECRET("18007", "Consumer secret/Client secret must not be null."), + AGENT_CONFIG_CALLBACK_URL("18008", "Callback URL/Redirection URL must not be null. This refers to the Relying Party's redirection URIs " + "registered with the OpenID Provider."), SERVLET_CONNECTION("18008", "Error found with connection."); diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/RequestContext.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/RequestContext.java new file mode 100644 index 0000000..1b9656d --- /dev/null +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/RequestContext.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.asgardio.java.oidc.sdk.bean; + +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.Nonce; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * A data model class to define the Request Context element. The Request Context object + * should be used to hold the attributes regarding the authentication flow. These include the attributes: + * + *

+ * The Request Context and its attributes would be used from the initiation of the authentication + * request until the authentication completion of the user. + */ +public class RequestContext implements Serializable { + + private static final long serialVersionUID = -3980859739213942559L; + + private State state; + private Nonce nonce; + private Map additionalParams = new HashMap<>(); + + public RequestContext(State state, Nonce nonce) { + + this.state = state; + this.nonce = nonce; + } + + public RequestContext() { + + } + + /** + * Returns the state. + * + * @return {@link State} object for the request. + */ + public State getState() { + + return state; + } + + /** + * Sets the state. + * + * @param state The state object. + */ + public void setState(State state) { + + this.state = state; + } + + /** + * Returns the nonce. + * + * @return {@link Nonce} object for the request. + */ + public Nonce getNonce() { + + return nonce; + } + + /** + * Sets the nonce. + * + * @param nonce The nonce object. + */ + public void setNonce(Nonce nonce) { + + this.nonce = nonce; + } + + /** + * Returns the object for the particular key. + * + * @param key The String value of the key. + * @return The additional parameter object in the request for the particular key. + */ + public Object getParameter(String key) { + + return additionalParams.get(key); + } + + /** + * Sets additional parameter to the Request Context. + * + * @param key The key of the parameter. + * @param value The value of the parameter. + */ + public void setParameter(String key, Object value) { + + additionalParams.put(key, value); + } +} diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/AuthenticationInfo.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/SessionContext.java similarity index 69% rename from io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/AuthenticationInfo.java rename to io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/SessionContext.java index 029158f..6612414 100644 --- a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/AuthenticationInfo.java +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/bean/SessionContext.java @@ -18,23 +18,27 @@ package io.asgardio.java.oidc.sdk.bean; -import com.nimbusds.jwt.JWT; -import com.nimbusds.oauth2.sdk.token.AccessToken; -import com.nimbusds.oauth2.sdk.token.RefreshToken; - import java.io.Serializable; /** - * A data model class to define the Authentication Info element. + * A data model class to define the Session Context element. The Session Context object should be used to hold the + * attributes of the logged in user session. These include the attributes: + *

+ *

*/ -public class AuthenticationInfo implements Serializable { +public class SessionContext implements Serializable { private static final long serialVersionUID = 976008884476935474L; private User user; - private AccessToken accessToken; - private RefreshToken refreshToken; - private JWT idToken; + private String accessToken; + private String refreshToken; + private String idToken; /** * Returns the authenticated user. @@ -59,9 +63,9 @@ public void setUser(User user) { /** * Returns the access token. * - * @return The {@link AccessToken}. + * @return The access token string. */ - public AccessToken getAccessToken() { + public String getAccessToken() { return accessToken; } @@ -71,7 +75,7 @@ public AccessToken getAccessToken() { * * @param accessToken The access token. */ - public void setAccessToken(AccessToken accessToken) { + public void setAccessToken(String accessToken) { this.accessToken = accessToken; } @@ -79,9 +83,9 @@ public void setAccessToken(AccessToken accessToken) { /** * Returns the refresh token. * - * @return The {@link RefreshToken}. + * @return The refresh token string. */ - public RefreshToken getRefreshToken() { + public String getRefreshToken() { return refreshToken; } @@ -91,7 +95,7 @@ public RefreshToken getRefreshToken() { * * @param refreshToken The refresh token. */ - public void setRefreshToken(RefreshToken refreshToken) { + public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } @@ -99,9 +103,9 @@ public void setRefreshToken(RefreshToken refreshToken) { /** * Returns the id token. * - * @return The {@link JWT} Id token. + * @return The Id token string. */ - public JWT getIdToken() { + public String getIdToken() { return idToken; } @@ -111,7 +115,7 @@ public JWT getIdToken() { * * @param idToken The id token. */ - public void setIdToken(JWT idToken) { + public void setIdToken(String idToken) { this.idToken = idToken; } diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/config/FileBasedOIDCConfigProvider.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/config/FileBasedOIDCConfigProvider.java index c69f417..fc70d11 100644 --- a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/config/FileBasedOIDCConfigProvider.java +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/config/FileBasedOIDCConfigProvider.java @@ -18,6 +18,7 @@ package io.asgardio.java.oidc.sdk.config; +import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.oauth2.sdk.Scope; import com.nimbusds.oauth2.sdk.auth.Secret; import com.nimbusds.oauth2.sdk.id.ClientID; @@ -34,6 +35,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; +import java.util.Collections; import java.util.HashSet; import java.util.Properties; import java.util.Set; @@ -66,9 +68,11 @@ private void initConfig(Properties properties) throws SSOAgentClientException { Secret consumerSecret = StringUtils.isNotBlank(properties.getProperty(SSOAgentConstants.CONSUMER_SECRET)) ? new Secret(properties.getProperty(SSOAgentConstants.CONSUMER_SECRET)) : null; String indexPage = properties.getProperty(SSOAgentConstants.INDEX_PAGE); + String errorPage = properties.getProperty(SSOAgentConstants.ERROR_PAGE); String logoutURL = properties.getProperty(SSOAgentConstants.LOGOUT_URL); - Issuer issuer = StringUtils.isNotBlank(properties.getProperty(SSOAgentConstants.OIDC_ISSUER)) ? - new Issuer(properties.getProperty(SSOAgentConstants.OIDC_ISSUER)) : null; + JWSAlgorithm jwsAlgorithm = + StringUtils.isNotBlank(properties.getProperty(SSOAgentConstants.ID_TOKEN_SIGN_ALG)) ? + new JWSAlgorithm(properties.getProperty(SSOAgentConstants.ID_TOKEN_SIGN_ALG)) : null; try { URI callbackUrl = StringUtils.isNotBlank(properties.getProperty(SSOAgentConstants.CALL_BACK_URL)) ? @@ -98,6 +102,8 @@ private void initConfig(Properties properties) throws SSOAgentClientException { throw new SSOAgentClientException("URL not formatted properly.", e); } + Issuer issuer = StringUtils.isNotBlank(properties.getProperty(SSOAgentConstants.OIDC_ISSUER)) ? + new Issuer(properties.getProperty(SSOAgentConstants.OIDC_ISSUER)) : null; String scopeString = properties.getProperty(SSOAgentConstants.SCOPE); if (StringUtils.isNotBlank(scopeString)) { String[] scopeArray = scopeString.split(","); @@ -109,17 +115,31 @@ private void initConfig(Properties properties) throws SSOAgentClientException { String skipURIsString = properties.getProperty(SSOAgentConstants.SKIP_URIS); if (StringUtils.isNotBlank(skipURIsString)) { String[] skipURIArray = skipURIsString.split(","); - for (String skipURI : skipURIArray) { - skipURIs.add(skipURI); - } + Collections.addAll(skipURIs, skipURIArray); + } + if (StringUtils.isNotBlank(indexPage)) { + skipURIs.add(indexPage); + } + if (StringUtils.isNotBlank(errorPage)) { + skipURIs.add(errorPage); + } + Set trustedAudience = new HashSet(); + trustedAudience.add(consumerKey.getValue()); + String trustedAudienceString = properties.getProperty(SSOAgentConstants.TRUSTED_AUDIENCE); + if (StringUtils.isNotBlank(trustedAudienceString)) { + String[] trustedAudienceArray = trustedAudienceString.split(","); + Collections.addAll(trustedAudience, trustedAudienceArray); } oidcAgentConfig.setConsumerKey(consumerKey); oidcAgentConfig.setConsumerSecret(consumerSecret); oidcAgentConfig.setIndexPage(indexPage); + oidcAgentConfig.setErrorPage(errorPage); oidcAgentConfig.setLogoutURL(logoutURL); oidcAgentConfig.setIssuer(issuer); oidcAgentConfig.setSkipURIs(skipURIs); + oidcAgentConfig.setTrustedAudience(trustedAudience); + oidcAgentConfig.setSignatureAlgorithm(jwsAlgorithm); } /** diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/config/model/OIDCAgentConfig.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/config/model/OIDCAgentConfig.java index e82c15c..7f9f3ea 100644 --- a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/config/model/OIDCAgentConfig.java +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/config/model/OIDCAgentConfig.java @@ -18,6 +18,7 @@ package io.asgardio.java.oidc.sdk.config.model; +import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.oauth2.sdk.Scope; import com.nimbusds.oauth2.sdk.auth.Secret; import com.nimbusds.oauth2.sdk.id.ClientID; @@ -35,6 +36,7 @@ public class OIDCAgentConfig { private ClientID consumerKey; private Secret consumerSecret; private String indexPage; + private String errorPage; private String logoutURL; private URI callbackUrl; private Scope scope; @@ -42,8 +44,10 @@ public class OIDCAgentConfig { private URI logoutEndpoint; private URI tokenEndpoint; private Issuer issuer; + private Set trustedAudience; private URI jwksEndpoint; private URI postLogoutRedirectURI; + private JWSAlgorithm signatureAlgorithm; private Set skipURIs = new HashSet(); /** @@ -106,6 +110,26 @@ public void setIndexPage(String indexPage) { this.indexPage = indexPage; } + /** + * Returns the error page of the OIDC agent. + * + * @return Error page of the OIDC agent. + */ + public String getErrorPage() { + + return errorPage; + } + + /** + * Sets the error page for the OIDC agent. + * + * @param errorPage The error page of the OIDC agent. + */ + public void setErrorPage(String errorPage) { + + this.errorPage = errorPage; + } + /** * Returns the logout URL of the OIDC agent. * @@ -246,6 +270,16 @@ public void setIssuer(Issuer issuer) { this.issuer = issuer; } + public Set getTrustedAudience() { + + return trustedAudience; + } + + public void setTrustedAudience(Set trustedAudience) { + + this.trustedAudience = trustedAudience; + } + /** * Returns the JWKS endpoint URI of the OIDC agent. * @@ -286,6 +320,16 @@ public void setPostLogoutRedirectURI(URI postLogoutRedirectURI) { this.postLogoutRedirectURI = postLogoutRedirectURI; } + public JWSAlgorithm getSignatureAlgorithm() { + + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(JWSAlgorithm signatureAlgorithm) { + + this.signatureAlgorithm = signatureAlgorithm; + } + /** * Returns the skip URIs of the OIDC agent. * diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/exception/SSOAgentServerException.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/exception/SSOAgentServerException.java index 7e5124c..22298b6 100644 --- a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/exception/SSOAgentServerException.java +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/exception/SSOAgentServerException.java @@ -26,6 +26,18 @@ public class SSOAgentServerException extends SSOAgentException { private static final long serialVersionUID = 4776260071061676883L; + /** + * Constructs a SSOAgentServerException with the specified detail + * message. A detail message is a String that describes this + * particular exception. + * + * @param message The detail message. + */ + public SSOAgentServerException(String message) { + + super(message); + } + /** * Creates a {@code SSOAgentServerException} with the specified * detail message and cause. diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/OIDCRequestBuilder.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/OIDCRequestBuilder.java index faf83a7..bc00890 100644 --- a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/OIDCRequestBuilder.java +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/OIDCRequestBuilder.java @@ -19,20 +19,25 @@ package io.asgardio.java.oidc.sdk.request; import com.nimbusds.jwt.JWT; -import com.nimbusds.oauth2.sdk.AuthorizationRequest; +import com.nimbusds.jwt.JWTParser; import com.nimbusds.oauth2.sdk.ResponseType; import com.nimbusds.oauth2.sdk.Scope; import com.nimbusds.oauth2.sdk.id.ClientID; import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.AuthenticationRequest; import com.nimbusds.openid.connect.sdk.LogoutRequest; +import com.nimbusds.openid.connect.sdk.Nonce; import io.asgardio.java.oidc.sdk.OIDCManager; -import io.asgardio.java.oidc.sdk.bean.AuthenticationInfo; +import io.asgardio.java.oidc.sdk.bean.RequestContext; +import io.asgardio.java.oidc.sdk.bean.SessionContext; import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; -import org.apache.commons.lang.StringUtils; +import io.asgardio.java.oidc.sdk.exception.SSOAgentServerException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.net.URI; +import java.text.ParseException; +import java.util.UUID; /** * OIDCRequestBuilder is the class responsible for building requests @@ -58,62 +63,83 @@ public OIDCRequestBuilder(OIDCAgentConfig oidcAgentConfig) { } /** - * Returns {@link String} Authorization request. To build the authorization request, - * {@link OIDCAgentConfig} should contain: + * Returns {@link io.asgardio.java.oidc.sdk.request.model.AuthenticationRequest} Authentication request. + * To build the authentication request, {@link OIDCAgentConfig} should contain: *

* - * @param state State parameter. - * @return Authorization request. + * @return Authentication request. */ - public String buildAuthorizationRequest(String state) { + public io.asgardio.java.oidc.sdk.request.model.AuthenticationRequest buildAuthenticationRequest() { ResponseType responseType = new ResponseType(ResponseType.Value.CODE); ClientID clientID = oidcAgentConfig.getConsumerKey(); Scope authScope = oidcAgentConfig.getScope(); URI callBackURI = oidcAgentConfig.getCallbackUrl(); URI authorizationEndpoint = oidcAgentConfig.getAuthorizeEndpoint(); - State stateParameter = null; - - if (StringUtils.isNotBlank(state)) { - stateParameter = new State(state); - } + State state = generateStateParameter(); + Nonce nonce = new Nonce(); + RequestContext requestContext = new RequestContext(state, nonce); - AuthorizationRequest authorizationRequest = new AuthorizationRequest.Builder(responseType, clientID) - .scope(authScope) - .state(stateParameter) - .redirectionURI(callBackURI) + AuthenticationRequest authenticationRequest = new AuthenticationRequest.Builder(responseType, authScope, + clientID, callBackURI) + .state(state) .endpointURI(authorizationEndpoint) + .nonce(nonce) .build(); - return authorizationRequest.toURI().toString(); + + io.asgardio.java.oidc.sdk.request.model.AuthenticationRequest authRequest = + new io.asgardio.java.oidc.sdk.request.model.AuthenticationRequest(authenticationRequest.toURI(), + requestContext); + + return authRequest; } /** - * Returns {@link String} Logout request. To build the logout request, + * Returns {@link io.asgardio.java.oidc.sdk.request.model.LogoutRequest} Logout request. To build the logout request, * {@link OIDCAgentConfig} should contain: * * - * @param authenticationInfo {@link AuthenticationInfo} object with information of the current LoggedIn session. - * It must include a valid ID token. - * @param state State parameter. + * @param sessionContext {@link SessionContext} object with information of the current LoggedIn session. + * It must include a valid ID token. * @return Logout request. */ - public String buildLogoutRequest(AuthenticationInfo authenticationInfo, String state) { + public io.asgardio.java.oidc.sdk.request.model.LogoutRequest buildLogoutRequest(SessionContext sessionContext) + throws SSOAgentServerException { URI logoutEP = oidcAgentConfig.getLogoutEndpoint(); URI redirectionURI = oidcAgentConfig.getPostLogoutRedirectURI(); - JWT jwtIdToken = authenticationInfo.getIdToken(); - State stateParam = null; + JWT jwtIdToken = null; + try { + jwtIdToken = JWTParser.parse(sessionContext.getIdToken()); + } catch (ParseException e) { + throw new SSOAgentServerException(e.getMessage(), e); + } + State state = generateStateParameter(); + RequestContext requestContext = new RequestContext(); - if (StringUtils.isNotBlank(state)) { - stateParam = new State(state); + requestContext.setState(state); + URI logoutRequestURI; + + try { + logoutRequestURI = new LogoutRequest(logoutEP, jwtIdToken, redirectionURI, state).toURI(); + } catch (Exception e) { + throw new SSOAgentServerException(e.getMessage(), e); } - return new LogoutRequest(logoutEP, jwtIdToken, redirectionURI, stateParam).toURI().toString(); + + return new io.asgardio.java.oidc.sdk.request.model.LogoutRequest(logoutRequestURI, requestContext); + } + + private State generateStateParameter() { + + UUID uuid = UUID.randomUUID(); + return new State(uuid.toString()); } } diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/model/AuthenticationRequest.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/model/AuthenticationRequest.java new file mode 100644 index 0000000..7f4c200 --- /dev/null +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/model/AuthenticationRequest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.asgardio.java.oidc.sdk.request.model; + +import io.asgardio.java.oidc.sdk.bean.RequestContext; + +import java.io.Serializable; +import java.net.URI; + +/** + * A data model class to define the Authentication Request element. + */ +public class AuthenticationRequest implements Serializable { + + private static final long serialVersionUID = 7931793096680065576L; + + private URI authenticationRequestURI; + private RequestContext requestContext; + + public AuthenticationRequest(URI authenticationRequestURI, RequestContext requestContext) { + + this.authenticationRequestURI = authenticationRequestURI; + this.requestContext = requestContext; + } + + public URI getAuthenticationRequestURI() { + + return authenticationRequestURI; + } + + public void setAuthenticationRequestURI(URI authenticationRequestURI) { + + this.authenticationRequestURI = authenticationRequestURI; + } + + public RequestContext getRequestContext() { + + return requestContext; + } + + public void setRequestContext(RequestContext requestContext) { + + this.requestContext = requestContext; + } +} diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/model/LogoutRequest.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/model/LogoutRequest.java new file mode 100644 index 0000000..f03cbb7 --- /dev/null +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/request/model/LogoutRequest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.asgardio.java.oidc.sdk.request.model; + +import io.asgardio.java.oidc.sdk.bean.RequestContext; + +import java.io.Serializable; +import java.net.URI; + +/** + * A data model class to define the Logout Request element. + */ +public class LogoutRequest implements Serializable { + + private static final long serialVersionUID = 6184960293632714833L; + + private URI logoutRequestURI; + private RequestContext requestContext; + + public LogoutRequest(URI logoutRequestURI, RequestContext requestContext) { + + this.logoutRequestURI = logoutRequestURI; + this.requestContext = requestContext; + } + + public URI getLogoutRequestURI() { + + return logoutRequestURI; + } + + public void setLogoutRequestURI(URI logoutRequestURI) { + + this.logoutRequestURI = logoutRequestURI; + } + + public RequestContext getRequestContext() { + + return requestContext; + } + + public void setRequestContext(RequestContext requestContext) { + + this.requestContext = requestContext; + } +} diff --git a/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/validators/IDTokenValidator.java b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/validators/IDTokenValidator.java new file mode 100644 index 0000000..2fd7957 --- /dev/null +++ b/io.asgardio.java.oidc.sdk/src/main/java/io/asgardio/java/oidc/sdk/validators/IDTokenValidator.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.asgardio.java.oidc.sdk.validators; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.JWT; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.Audience; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet; +import io.asgardio.java.oidc.sdk.SSOAgentConstants; +import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; +import io.asgardio.java.oidc.sdk.exception.SSOAgentServerException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.net.MalformedURLException; +import java.net.URI; +import java.util.List; +import java.util.Set; + +/** + * Validator of ID tokens issued by an OpenID Provider. + * + *

Supports processing of ID tokens with: + * + *

+ */ +public class IDTokenValidator { + + private static final Logger logger = LogManager.getLogger(IDTokenValidator.class); + + private OIDCAgentConfig oidcAgentConfig; + private JWT idToken; + + public IDTokenValidator(OIDCAgentConfig oidcAgentConfig, JWT idToken) { + + this.oidcAgentConfig = oidcAgentConfig; + this.idToken = idToken; + } + + public IDTokenClaimsSet validate(Nonce expectedNonce) throws SSOAgentServerException { + + JWSAlgorithm jwsAlgorithm = validateJWSAlgorithm(oidcAgentConfig, idToken); + com.nimbusds.openid.connect.sdk.validators.IDTokenValidator validator = + getIDTokenValidator(oidcAgentConfig, jwsAlgorithm); + IDTokenClaimsSet claims; + try { + claims = validator.validate(idToken, expectedNonce); + validateAudience(oidcAgentConfig, claims); + } catch (JOSEException | BadJOSEException e) { + throw new SSOAgentServerException(e.getMessage(), e.getCause()); + } + return claims; + } + + private com.nimbusds.openid.connect.sdk.validators.IDTokenValidator getIDTokenValidator( + OIDCAgentConfig oidcAgentConfig, JWSAlgorithm jwsAlgorithm) throws SSOAgentServerException { + + Issuer issuer = oidcAgentConfig.getIssuer(); + URI jwkSetURI = oidcAgentConfig.getJwksEndpoint(); + ClientID clientID = oidcAgentConfig.getConsumerKey(); + Secret clientSecret = oidcAgentConfig.getConsumerSecret(); + com.nimbusds.openid.connect.sdk.validators.IDTokenValidator validator; + + // Creates a new validator for RSA, EC or ED protected ID tokens. + if (JWSAlgorithm.Family.RSA.contains(jwsAlgorithm) || JWSAlgorithm.Family.EC.contains(jwsAlgorithm) || + JWSAlgorithm.Family.ED.contains(jwsAlgorithm)) { + try { + validator = + new com.nimbusds.openid.connect.sdk.validators.IDTokenValidator(issuer, clientID, jwsAlgorithm, + jwkSetURI.toURL()); + } catch (Exception e) { + throw new SSOAgentServerException(e.getMessage(), e.getCause()); + } + // Creates a new validator for HMAC protected ID tokens. + } else if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) { + validator = new com.nimbusds.openid.connect.sdk.validators.IDTokenValidator(issuer, clientID, jwsAlgorithm, + clientSecret); + } else { + throw new SSOAgentServerException(String.format("Unsupported algorithm: %s.", jwsAlgorithm.getName())); + } + return validator; + } + + private JWSAlgorithm validateJWSAlgorithm(OIDCAgentConfig oidcAgentConfig, JWT idToken) + throws SSOAgentServerException { + + JWSAlgorithm jwsAlgorithm = (JWSAlgorithm) idToken.getHeader().getAlgorithm(); + JWSAlgorithm expectedJWSAlgorithm = oidcAgentConfig.getSignatureAlgorithm(); + + if (expectedJWSAlgorithm == null) { + if (JWSAlgorithm.RS256.equals(jwsAlgorithm)) { + return jwsAlgorithm; + } else { + throw new SSOAgentServerException(String.format("Signed JWT rejected. Provided signature algorithm: " + + "%s is not the default of RS256.", jwsAlgorithm.getName())); + } + } else if (!expectedJWSAlgorithm.equals(jwsAlgorithm)) { + throw new SSOAgentServerException(String.format("Signed JWT rejected: Another algorithm expected. " + + "Provided signature algorithm: %s.", jwsAlgorithm.getName())); + } + return jwsAlgorithm; + } + + private void validateAudience(OIDCAgentConfig oidcAgentConfig, IDTokenClaimsSet claimsSet) + throws SSOAgentServerException { + + List audience = claimsSet.getAudience(); + if (audience.size() > 1) { + if (claimsSet.getClaim(SSOAgentConstants.AZP) == null) { + throw new SSOAgentServerException("ID token validation failed. AZP claim cannot be null for multiple " + + "audiences."); + } + Set trustedAudience = oidcAgentConfig.getTrustedAudience(); + for (Audience aud : audience) { + if (!trustedAudience.contains(aud.getValue())) { + throw new SSOAgentServerException("ID token validation failed. Untrusted JWT audience."); + } + } + } + } +} diff --git a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/OIDCManagerImplTest.java b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/DefaultOIDCManagerTest.java similarity index 67% rename from io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/OIDCManagerImplTest.java rename to io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/DefaultOIDCManagerTest.java index f55c9d6..8e79822 100644 --- a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/OIDCManagerImplTest.java +++ b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/DefaultOIDCManagerTest.java @@ -24,7 +24,6 @@ import com.nimbusds.oauth2.sdk.AuthorizationCode; import com.nimbusds.oauth2.sdk.AuthorizationResponse; import com.nimbusds.oauth2.sdk.AuthorizationSuccessResponse; -import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.Scope; import com.nimbusds.oauth2.sdk.TokenResponse; import com.nimbusds.oauth2.sdk.auth.Secret; @@ -32,22 +31,34 @@ import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.http.ServletUtils; import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.oauth2.sdk.id.Subject; import com.nimbusds.oauth2.sdk.token.AccessToken; import com.nimbusds.oauth2.sdk.token.AccessTokenType; import com.nimbusds.oauth2.sdk.token.RefreshToken; import com.nimbusds.oauth2.sdk.token.Tokens; -import io.asgardio.java.oidc.sdk.bean.AuthenticationInfo; +import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet; +import io.asgardio.java.oidc.sdk.bean.RequestContext; +import io.asgardio.java.oidc.sdk.bean.SessionContext; import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; import io.asgardio.java.oidc.sdk.exception.SSOAgentException; import io.asgardio.java.oidc.sdk.request.OIDCRequestResolver; +import io.asgardio.java.oidc.sdk.validators.IDTokenValidator; import org.mockito.Mock; import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockserver.integration.ClientAndServer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.testng.PowerMockTestCase; +import org.testng.IObjectFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.ObjectFactory; import org.testng.annotations.Test; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; @@ -55,6 +66,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -62,7 +74,9 @@ import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; -public class OIDCManagerImplTest { +@PrepareForTest({IDTokenValidator.class, IDTokenClaimsSet.class, + com.nimbusds.openid.connect.sdk.validators.IDTokenValidator.class}) +public class DefaultOIDCManagerTest extends PowerMockTestCase { @Mock HttpServletRequest request; @@ -74,20 +88,22 @@ public class OIDCManagerImplTest { OIDCRequestResolver requestResolver; @Mock - AuthenticationInfo authenticationInfo; + SessionContext sessionContext; OIDCAgentConfig oidcAgentConfig = new OIDCAgentConfig(); private ClientAndServer mockServer; @BeforeMethod - public void setUp() throws URISyntaxException, java.text.ParseException { + public void setUp() throws Exception { - mockServer = ClientAndServer.startClientAndServer(9443); + mockServer = ClientAndServer.startClientAndServer(9441); + Issuer issuer = new Issuer("issuer"); ClientID clientID = new ClientID("sampleClientId"); - Secret clientSecret = new Secret("sampleClientSceret"); - URI callbackURI = new URI("http://localhost:9443/sampleCallbackURL"); - URI tokenEPURI = new URI("http://localhost:9443/sampleTokenEP"); + Secret clientSecret = new Secret("sampleClientSecret"); + URI callbackURI = new URI("http://localhost:9441/sampleCallbackURL"); + URI tokenEPURI = new URI("http://localhost:9441/sampleTokenEP"); + URI jwksURI = new URI("http://localhost:9441/jwksEP"); URI logoutEP = new URI("http://test/sampleLogoutEP"); Scope scope = new Scope("sampleScope1", "openid"); JWT idToken = JWTParser @@ -97,7 +113,7 @@ public void setUp() throws URISyntaxException, java.text.ParseException { request = mock(HttpServletRequest.class); response = mock(HttpServletResponse.class); requestResolver = mock(OIDCRequestResolver.class); - authenticationInfo = mock(AuthenticationInfo.class); + sessionContext = mock(SessionContext.class); oidcAgentConfig.setConsumerKey(clientID); oidcAgentConfig.setConsumerSecret(clientSecret); @@ -105,11 +121,23 @@ public void setUp() throws URISyntaxException, java.text.ParseException { oidcAgentConfig.setTokenEndpoint(tokenEPURI); oidcAgentConfig.setLogoutEndpoint(logoutEP); oidcAgentConfig.setScope(scope); - when(authenticationInfo.getIdToken()).thenReturn(idToken); + oidcAgentConfig.setIssuer(issuer); + oidcAgentConfig.setJwksEndpoint(jwksURI); + when(sessionContext.getIdToken()).thenReturn(idToken.getParsedString()); + IDTokenClaimsSet claimsSet = mock(IDTokenClaimsSet.class); + IDTokenValidator idTokenValidator = mock(IDTokenValidator.class); + com.nimbusds.openid.connect.sdk.validators.IDTokenValidator validator = mock( + com.nimbusds.openid.connect.sdk.validators.IDTokenValidator.class); + PowerMockito.whenNew(IDTokenValidator.class).withAnyArguments().thenReturn(idTokenValidator); + PowerMockito.whenNew(com.nimbusds.openid.connect.sdk.validators.IDTokenValidator.class).withAnyArguments() + .thenReturn(validator); + when(validator.validate(any(JWT.class), any(Nonce.class))).thenReturn(claimsSet); + Mockito.when(idTokenValidator.validate(any(Nonce.class))).thenReturn(claimsSet); + Mockito.when(claimsSet.getSubject()).thenReturn(new Subject("alex@carbon.super")); } @Test - public void testHandleOIDCCallback() throws SSOAgentException, IOException, ParseException { + public void testHandleOIDCCallback() throws Exception { AccessToken accessToken = new AccessToken(AccessTokenType.BEARER, "sampleAccessToken") { @Override @@ -132,6 +160,7 @@ public String toAuthorizationHeader() { "hAd3NvMi5jb20ifQ.pHwsQqn64tif2J6iYcRShK_85WO3aBuL7Pz8urcHErXjyh6zvroOqSWD9KbSxJPocyoIshdqWdAEhdURKL" + "tXiw-l73HlvnX4qJKYT71VKXMTC26Z8dlk4TgytXiskmj8OpAcem3czuEWTrTLVbYzIw71p9kx-5Xxb9WNvzBg1YpwGC8MK3dkW" + "TfmUsu6oncIvHyv-gbX3kJebgMserp"; + JWT idToken = JWTParser.parse(parsedIdToken); customParameters.put(SSOAgentConstants.ID_TOKEN, parsedIdToken); when(requestResolver.isError()).thenReturn(false); @@ -156,15 +185,18 @@ public String toAuthorizationHeader() { when(tokenResponse.toSuccessResponse()).thenReturn(accessTokenResponse); when(accessTokenResponse.getTokens()).thenReturn(tokens); when(accessTokenResponse.getCustomParameters()).thenReturn(customParameters); - - OIDCManager oidcManager = new OIDCManagerImpl(oidcAgentConfig); - AuthenticationInfo authenticationInfo = oidcManager.handleOIDCCallback(request, response); - - assertEquals(authenticationInfo.getAccessToken(), accessToken); - assertEquals(authenticationInfo.getRefreshToken(), refreshToken); - assertEquals(authenticationInfo.getIdToken().getParsedString(), parsedIdToken); - assertEquals(authenticationInfo.getUser().getSubject(), "alex@carbon.super"); - + HttpSession session = mock(HttpSession.class); + when(request.getSession(false)).thenReturn(session); + when(session.getAttribute(SSOAgentConstants.NONCE)).thenReturn(new Nonce()); + RequestContext requestContext = new RequestContext(new State("state"), new Nonce()); + + OIDCManager oidcManager = new DefaultOIDCManager(oidcAgentConfig); + SessionContext sessionContext = oidcManager.handleOIDCCallback(request, response, requestContext); + + assertEquals(sessionContext.getAccessToken(), accessToken.toJSONString()); + assertEquals(sessionContext.getRefreshToken(), refreshToken.getValue()); + assertEquals(sessionContext.getIdToken(), parsedIdToken); + assertEquals(sessionContext.getUser().getSubject(), "alex@carbon.super"); mockedAuthorizationResponse.close(); mockedServletUtils.close(); mockedTokenResponse.close(); @@ -174,8 +206,8 @@ public String toAuthorizationHeader() { public void testLogoutCallbackURI() throws SSOAgentException { oidcAgentConfig.setPostLogoutRedirectURI(null); - OIDCManager oidcManager = new OIDCManagerImpl(oidcAgentConfig); - oidcManager.logout(authenticationInfo, response, "state"); + OIDCManager oidcManager = new DefaultOIDCManager(oidcAgentConfig); + oidcManager.logout(sessionContext, response); } @Test @@ -183,8 +215,8 @@ public void testLogoutRedirectURI() throws URISyntaxException, SSOAgentException URI redirectionURI = new URI("http://test/sampleRedirectionURL"); oidcAgentConfig.setPostLogoutRedirectURI(redirectionURI); - OIDCManager oidcManager = new OIDCManagerImpl(oidcAgentConfig); - oidcManager.logout(authenticationInfo, response, "state"); + OIDCManager oidcManager = new DefaultOIDCManager(oidcAgentConfig); + oidcManager.logout(sessionContext, response); } @AfterMethod @@ -192,4 +224,10 @@ public void tearDown() { mockServer.stop(); } + + @ObjectFactory + public IObjectFactory getObjectFactory() { + + return new org.powermock.modules.testng.PowerMockObjectFactory(); + } } diff --git a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/HTTPSessionBasedOIDCProcessorTest.java b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/HTTPSessionBasedOIDCProcessorTest.java new file mode 100644 index 0000000..d7afc42 --- /dev/null +++ b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/HTTPSessionBasedOIDCProcessorTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.asgardio.java.oidc.sdk; + +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.Nonce; +import io.asgardio.java.oidc.sdk.bean.RequestContext; +import io.asgardio.java.oidc.sdk.bean.SessionContext; +import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; +import io.asgardio.java.oidc.sdk.exception.SSOAgentClientException; +import io.asgardio.java.oidc.sdk.exception.SSOAgentException; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.testng.PowerMockTestCase; +import org.testng.IObjectFactory; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.ObjectFactory; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@PrepareForTest({DefaultOIDCManager.class, DefaultOIDCManagerFactory.class}) +public class HTTPSessionBasedOIDCProcessorTest extends PowerMockTestCase { + + @Mock + DefaultOIDCManager defaultOIDCManager; + + @Mock + HttpServletRequest request; + + @Mock + HttpServletResponse response; + + OIDCAgentConfig oidcAgentConfig = new OIDCAgentConfig(); + + private static MockedStatic mockedOIDCManagerFactory; + + @BeforeMethod + public void setUp() throws Exception { + + defaultOIDCManager = mock(DefaultOIDCManager.class); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + } + + @AfterMethod + public void tearDown() { + + mockedOIDCManagerFactory.close(); + } + + @Test + public void testSendForLogin() throws Exception { + + Nonce nonce = new Nonce(); + State state = new State("SampleState"); + RequestContext requestContext = new RequestContext(state, nonce); + + HttpSession session = mock(HttpSession.class); + mockedOIDCManagerFactory = mockStatic(DefaultOIDCManagerFactory.class); + when(DefaultOIDCManagerFactory.createOIDCManager(oidcAgentConfig)).thenReturn(defaultOIDCManager); + when(request.getSession()).thenReturn(session); + when(defaultOIDCManager.sendForLogin(request, response)).thenReturn(requestContext); + + HTTPSessionBasedOIDCProcessor provider = new HTTPSessionBasedOIDCProcessor(oidcAgentConfig); + provider.sendForLogin(request, response); + + verify(session).setAttribute(SSOAgentConstants.REQUEST_CONTEXT, requestContext); + } + + @Test + public void testHandleOIDCCallback() throws SSOAgentException { + + SessionContext sessionContext = new SessionContext(); + RequestContext requestContext = new RequestContext(); + + HttpSession session = mock(HttpSession.class); + mockedOIDCManagerFactory = mockStatic(DefaultOIDCManagerFactory.class); + when(DefaultOIDCManagerFactory.createOIDCManager(oidcAgentConfig)).thenReturn(defaultOIDCManager); + when(request.getSession()).thenReturn(session); + when(request.getSession(false)).thenReturn(session); + when(session.getAttribute(SSOAgentConstants.REQUEST_CONTEXT)).thenReturn(requestContext); + when(defaultOIDCManager.handleOIDCCallback(request, response, requestContext)).thenReturn(sessionContext); + + HTTPSessionBasedOIDCProcessor provider = new HTTPSessionBasedOIDCProcessor(oidcAgentConfig); + provider.handleOIDCCallback(request, response); + + verify(session).setAttribute(SSOAgentConstants.SESSION_CONTEXT, sessionContext); + } + + @Test + public void testLogout() throws SSOAgentException { + + RequestContext requestContext = new RequestContext(); + SessionContext sessionContext = new SessionContext(); + + HttpSession session = mock(HttpSession.class); + mockedOIDCManagerFactory = mockStatic(DefaultOIDCManagerFactory.class); + when(DefaultOIDCManagerFactory.createOIDCManager(oidcAgentConfig)).thenReturn(defaultOIDCManager); + when(request.getSession()).thenReturn(session); + when(request.getSession(false)).thenReturn(session); + when(session.getAttribute(SSOAgentConstants.SESSION_CONTEXT)).thenReturn(sessionContext); + when(defaultOIDCManager.logout(sessionContext, response)).thenReturn(requestContext); + + HTTPSessionBasedOIDCProcessor provider = new HTTPSessionBasedOIDCProcessor(oidcAgentConfig); + provider.logout(request, response); + + verify(session).setAttribute(SSOAgentConstants.REQUEST_CONTEXT, requestContext); + } + + @ObjectFactory + public IObjectFactory getObjectFactory() { + + return new org.powermock.modules.testng.PowerMockObjectFactory(); + } +} diff --git a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/bean/AuthenticationInfoTest.java b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/bean/AuthenticationInfoTest.java index 1f96268..3e2aec2 100644 --- a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/bean/AuthenticationInfoTest.java +++ b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/bean/AuthenticationInfoTest.java @@ -36,7 +36,7 @@ */ public class AuthenticationInfoTest { - AuthenticationInfo authenticationInfo = new AuthenticationInfo(); + SessionContext authenticationInfo = new SessionContext(); @Test public void testGetUser() { @@ -57,16 +57,16 @@ public String toAuthorizationHeader() { return null; } }; - authenticationInfo.setAccessToken(accessToken); - assertEquals(authenticationInfo.getAccessToken(), accessToken); + authenticationInfo.setAccessToken(accessToken.toJSONString()); + assertEquals(authenticationInfo.getAccessToken(), accessToken.toJSONString()); } @Test public void testGetRefreshToken() { RefreshToken refreshToken = new RefreshToken(); - authenticationInfo.setRefreshToken(refreshToken); - assertEquals(authenticationInfo.getRefreshToken(), refreshToken); + authenticationInfo.setRefreshToken(refreshToken.getValue()); + assertEquals(authenticationInfo.getRefreshToken(), refreshToken.getValue()); } @Test @@ -74,8 +74,8 @@ public void testGetIdToken() { try { JWT idToken = JWTParser.parse("sample"); - authenticationInfo.setIdToken(idToken); - assertEquals(authenticationInfo.getIdToken(), idToken); + authenticationInfo.setIdToken(idToken.getParsedString()); + assertEquals(authenticationInfo.getIdToken(), idToken.getParsedString()); } catch (ParseException e) { //Test behaviour. Hence ignored. } diff --git a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/request/OIDCRequestBuilderTest.java b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/request/OIDCRequestBuilderTest.java index ab27b49..acc9d2e 100644 --- a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/request/OIDCRequestBuilderTest.java +++ b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/request/OIDCRequestBuilderTest.java @@ -22,10 +22,18 @@ import com.nimbusds.jwt.JWTParser; import com.nimbusds.oauth2.sdk.Scope; import com.nimbusds.oauth2.sdk.id.ClientID; -import io.asgardio.java.oidc.sdk.bean.AuthenticationInfo; +import io.asgardio.java.oidc.sdk.bean.RequestContext; +import io.asgardio.java.oidc.sdk.bean.SessionContext; import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; +import io.asgardio.java.oidc.sdk.exception.SSOAgentServerException; +import io.asgardio.java.oidc.sdk.request.model.AuthenticationRequest; +import io.asgardio.java.oidc.sdk.request.model.LogoutRequest; import org.mockito.Mock; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.testng.PowerMockTestCase; +import org.testng.IObjectFactory; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.ObjectFactory; import org.testng.annotations.Test; import java.net.URI; @@ -36,19 +44,20 @@ import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; -public class OIDCRequestBuilderTest { +@PrepareForTest({OIDCAgentConfig.class, SessionContext.class}) +public class OIDCRequestBuilderTest extends PowerMockTestCase { @Mock OIDCAgentConfig oidcAgentConfig; @Mock - AuthenticationInfo authenticationInfo; + SessionContext sessionContext; @BeforeMethod public void setUp() throws URISyntaxException, ParseException { ClientID clientID = new ClientID("sampleClientId"); - Scope scope = new Scope("sampleScope1", "sampleScope2"); + Scope scope = new Scope("sampleScope1", "openid"); URI callbackURI = new URI("http://test/sampleCallbackURL"); URI authorizationEndpoint = new URI("http://test/sampleAuthzEP"); URI logoutEP = new URI("http://test/sampleLogoutEP"); @@ -58,7 +67,7 @@ public void setUp() throws URISyntaxException, ParseException { "WF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); oidcAgentConfig = mock(OIDCAgentConfig.class); - authenticationInfo = mock(AuthenticationInfo.class); + sessionContext = mock(SessionContext.class); when(oidcAgentConfig.getConsumerKey()).thenReturn(clientID); when(oidcAgentConfig.getScope()).thenReturn(scope); @@ -66,24 +75,39 @@ public void setUp() throws URISyntaxException, ParseException { when(oidcAgentConfig.getAuthorizeEndpoint()).thenReturn(authorizationEndpoint); when(oidcAgentConfig.getLogoutEndpoint()).thenReturn(logoutEP); when(oidcAgentConfig.getPostLogoutRedirectURI()).thenReturn(redirectionURI); - when(authenticationInfo.getIdToken()).thenReturn(idToken); + when(sessionContext.getIdToken()).thenReturn(idToken.getParsedString()); } @Test public void testBuildAuthorizationRequest() { - String authorizationRequest = new OIDCRequestBuilder(oidcAgentConfig).buildAuthorizationRequest("state"); - assertEquals(authorizationRequest, "http://test/sampleAuthzEP?response_type=code&redirect_uri=http%3A%2F" + - "%2Ftest%2FsampleCallbackURL&state=state&client_id=sampleClientId&scope=sampleScope1+sampleScope2"); + AuthenticationRequest authenticationRequest = + new OIDCRequestBuilder(oidcAgentConfig).buildAuthenticationRequest(); + RequestContext requestContext = authenticationRequest.getRequestContext(); + String nonce = requestContext.getNonce().getValue(); + String state = requestContext.getState().getValue(); + + assertEquals(authenticationRequest.getAuthenticationRequestURI().toString(), + "http://test/sampleAuthzEP?scope=sampleScope1+openid&response_type=code&redirect_uri=http" + + "%3A%2F%2Ftest%2FsampleCallbackURL&state=" + state + "&nonce=" + nonce + "&client_id" + + "=sampleClientId"); } @Test - public void testBuildLogoutRequest() { + public void testBuildLogoutRequest() throws SSOAgentServerException { + + LogoutRequest logoutRequest = new OIDCRequestBuilder(oidcAgentConfig).buildLogoutRequest(sessionContext); + RequestContext requestContext = logoutRequest.getRequestContext(); + String state = requestContext.getState().getValue(); + assertEquals(logoutRequest.getLogoutRequestURI().toString(), "http://test/sampleLogoutEP?state=" + state + + "&post_logout_redirect_uri=http%3A%2F%2Ftest%2FsampleRedirectionURL&id_token_hint=eyJhbGciOiJIUzI1NiI" + + "sInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwR" + + "JSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + } + + @ObjectFactory + public IObjectFactory getObjectFactory() { - String logoutRequest = new OIDCRequestBuilder(oidcAgentConfig).buildLogoutRequest(authenticationInfo, - "state"); - assertEquals(logoutRequest, "http://test/sampleLogoutEP?state=state&post_logout_redirect_uri=http%3A%2F%2" + - "Ftest%2FsampleRedirectionURL&id_token_hint=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3O" + - "DkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + return new org.powermock.modules.testng.PowerMockObjectFactory(); } } diff --git a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/request/OIDCRequestResolverTest.java b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/request/OIDCRequestResolverTest.java index 4e2ad16..a00aa8a 100644 --- a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/request/OIDCRequestResolverTest.java +++ b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/request/OIDCRequestResolverTest.java @@ -27,9 +27,12 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.testng.PowerMockTestCase; +import org.testng.IObjectFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; +import org.testng.annotations.ObjectFactory; import org.testng.annotations.Test; import java.io.IOException; @@ -47,7 +50,7 @@ import static org.testng.Assert.assertTrue; @PrepareForTest({AuthorizationResponse.class}) -public class OIDCRequestResolverTest { +public class OIDCRequestResolverTest extends PowerMockTestCase { @Mock OIDCAgentConfig oidcAgentConfig; @@ -149,4 +152,10 @@ public void testGetIndexPage(String indexPageConfig, String contextPath, String public void tearDown() { } + + @ObjectFactory + public IObjectFactory getObjectFactory() { + + return new org.powermock.modules.testng.PowerMockObjectFactory(); + } } diff --git a/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/validators/IDTokenValidatorTest.java b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/validators/IDTokenValidatorTest.java new file mode 100644 index 0000000..ee92c63 --- /dev/null +++ b/io.asgardio.java.oidc.sdk/src/test/java/io/asgardio/java/oidc/sdk/validators/IDTokenValidatorTest.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.asgardio.java.oidc.sdk.validators; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.Audience; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet; +import io.asgardio.java.oidc.sdk.config.model.OIDCAgentConfig; +import io.asgardio.java.oidc.sdk.exception.SSOAgentServerException; +import net.jadler.Jadler; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.net.URL; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.ECParameterSpec; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static net.jadler.Jadler.closeJadler; +import static net.jadler.Jadler.initJadler; +import static net.jadler.Jadler.port; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class IDTokenValidatorTest { + + private OIDCAgentConfig config; + private RSAKey key; + + @BeforeMethod + public void setUp() throws Exception { + + initJadler(); + + config = new OIDCAgentConfig(); + JWKSet jwkSet = generateJWKS(); + key = (RSAKey) jwkSet.getKeys().get(0); + + Issuer issuer = new Issuer("issuer"); + ClientID clientID = new ClientID("sampleClientId"); + Secret clientSecret = new Secret("sampleClientSecret"); + URL jwkSetURL = new URL("http://localhost:" + port() + "/jwksEP"); + + config.setIssuer(issuer); + config.setConsumerKey(clientID); + config.setConsumerSecret(clientSecret); + config.setJwksEndpoint(jwkSetURL.toURI()); + + Jadler.onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/jwksEP") + .respond() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(jwkSet.toJSONObject(true).toJSONString()); + } + + private JWKSet generateJWKS() throws NoSuchAlgorithmException { + + KeyPairGenerator pairGen = KeyPairGenerator.getInstance("RSA"); + pairGen.initialize(2048); + KeyPair keyPair = pairGen.generateKeyPair(); + + RSAKey rsaJWK1 = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) + .privateKey((RSAPrivateKey) keyPair.getPrivate()) + .keyID("1") + .build(); + + keyPair = pairGen.generateKeyPair(); + + RSAKey rsaJWK2 = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) + .privateKey((RSAPrivateKey) keyPair.getPrivate()) + .keyID("2") + .build(); + + JWKSet jwkSet = new JWKSet(Arrays.asList((JWK) rsaJWK1, (JWK) rsaJWK2)); + return jwkSet; + } + + private com.nimbusds.jose.jwk.ECKey generateECJWK(final Curve curve) throws Exception { + + ECParameterSpec ecParameterSpec = curve.toECParameterSpec(); + + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(ecParameterSpec); + KeyPair keyPair = generator.generateKeyPair(); + + return new com.nimbusds.jose.jwk.ECKey.Builder(curve, (ECPublicKey) keyPair.getPublic()). + privateKey((ECPrivateKey) keyPair.getPrivate()). + build(); + } + + @DataProvider(name = "IssuerData") + public Object[][] issuerData() { + + Issuer issuer1 = new Issuer("issuer1"); + Issuer issuer2 = new Issuer("issuer2"); + + return new Object[][]{ + // issuer + // expected + {issuer1, "issuer1"}, + {issuer2, "issuer2"} + }; + } + + @Test(dataProvider = "IssuerData") + public void testIssuer(Issuer issuer, String expectedIssuer) throws SSOAgentServerException, JOSEException { + + config.setIssuer(issuer); + Nonce nonce = new Nonce(); + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer(issuer.getValue()) + .subject("alice") + .audience(config.getConsumerKey().getValue()) + .expirationTime(new Date()) + .issueTime(new Date()) + .claim("nonce", nonce.getValue()) + .build(); + + SignedJWT idToken = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claims); + JWSSigner signer = new RSASSASigner(key); + idToken.sign(signer); + + IDTokenValidator validator = new IDTokenValidator(config, idToken); + IDTokenClaimsSet claimsSet = validator.validate(nonce); + assertEquals(claimsSet.getIssuer().getValue(), expectedIssuer); + } + + @DataProvider(name = "AudienceData") + public Object[][] audienceData() { + + String clientID1 = "clientID1"; + List tokenAudience1 = Arrays.asList(clientID1); + Set trustedAudience1 = new HashSet<>(tokenAudience1); + trustedAudience1.add(clientID1); + String azp1 = null; + + String clientID2 = "clientID2"; + List tokenAudience2 = Arrays.asList("aud1", "aud2", "aud3", clientID2); + Set trustedAudience2 = new HashSet<>(tokenAudience2); + String azp2 = clientID2; + + return new Object[][]{ + // token audience + // trusted audience + // client ID + // AZP value + {tokenAudience1, trustedAudience1, clientID1, azp1}, + {tokenAudience2, trustedAudience2, clientID2, azp2} + }; + } + + @Test(dataProvider = "AudienceData") + public void testAudience(List audience, Set trustedAudience, String clientID, String azpValue) + throws SSOAgentServerException, JOSEException { + + Nonce nonce = new Nonce(); + config.setTrustedAudience(trustedAudience); + config.setConsumerKey(new ClientID(clientID)); + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer(config.getIssuer().getValue()) + .subject("alice") + .audience(audience) + .expirationTime(new Date()) + .issueTime(new Date()) + .claim("nonce", nonce.getValue()) + .claim("azp", azpValue) + .build(); + + SignedJWT idToken = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claims); + JWSSigner signer = new RSASSASigner(key); + idToken.sign(signer); + + IDTokenValidator validator = new IDTokenValidator(config, idToken); + IDTokenClaimsSet claimsSet = validator.validate(nonce); + List audiences = claimsSet.getAudience(); + audiences.forEach(aud -> assertTrue(trustedAudience.contains(aud.getValue()))); + } + + @DataProvider(name = "AlgorithmData") + public Object[][] algorithmData() throws Exception { + + KeyPairGenerator pairGenRSA = KeyPairGenerator.getInstance("RSA"); + pairGenRSA.initialize(2048); + KeyPair keyPairRSA = pairGenRSA.generateKeyPair(); + + RSAKey rsaJWK = new RSAKey.Builder((RSAPublicKey) keyPairRSA.getPublic()) + .privateKey((RSAPrivateKey) keyPairRSA.getPrivate()) + .keyID("1") + .build(); + + ECKey ecJWK = generateECJWK(Curve.P_256); + + return new Object[][]{ + // algorithm + // key + {"RS256", (JWK) rsaJWK}, + {"ES256", (JWK) ecJWK} + }; + } + + @Test(dataProvider = "AlgorithmData") + public void testJWSAlgorithm(String signatureAlgorithm, JWK key) throws JOSEException, SSOAgentServerException { + + JWKSet jwkSet = new JWKSet(Collections.singletonList(key)); + + Jadler.onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/jwksEP") + .respond() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(jwkSet.toJSONObject(true).toJSONString()); + + Nonce nonce = new Nonce(); + JWSAlgorithm jwsAlgorithm = new JWSAlgorithm(signatureAlgorithm); + config.setSignatureAlgorithm(jwsAlgorithm); + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer(config.getIssuer().getValue()) + .subject("alice") + .audience(config.getConsumerKey().getValue()) + .expirationTime(new Date()) + .issueTime(new Date()) + .claim("nonce", nonce.getValue()) + .build(); + + SignedJWT idToken = new SignedJWT(new JWSHeader(jwsAlgorithm), claims); + JWSSigner signer; + if (key instanceof RSAKey) { + signer = new RSASSASigner((RSAKey) key); + } else { + signer = new ECDSASigner((ECKey) key); + } + idToken.sign(signer); + + IDTokenValidator validator = new IDTokenValidator(config, idToken); + IDTokenClaimsSet claimsSet = validator.validate(nonce); + assertEquals(claimsSet.getNonce(), nonce); + } + + @AfterMethod + public void tearDown() { + + closeJadler(); + } +} diff --git a/io.asgardio.java.oidc.sdk/src/test/resources/testng.xml b/io.asgardio.java.oidc.sdk/src/test/resources/testng.xml index 1610a10..fc82eaf 100644 --- a/io.asgardio.java.oidc.sdk/src/test/resources/testng.xml +++ b/io.asgardio.java.oidc.sdk/src/test/resources/testng.xml @@ -25,6 +25,12 @@ + + + + + + diff --git a/pom.xml b/pom.xml index 4233e58..54488c6 100644 --- a/pom.xml +++ b/pom.xml @@ -186,6 +186,22 @@ mockserver-client-java ${mockserver.version} + + org.powermock + powermock-api-mockito2 + ${mockito.api.version} + test + + + net.jadler + jadler-core + ${jadler.version} + + + net.jadler + jadler-jetty + ${jadler.version} + @@ -270,8 +286,9 @@ 7.3.0 2.0.7 3.5.13 - 1.7.4 + 2.0.7 3.10.8 + 1.3.0