Extend the user session between applications using WSO2 Identity Server

Lakshitha Samarasingha
7 min readMay 29, 2021

Background of the problem.

A user needs to access an application without providing credentials again, if he has already signed in with a different application which is running on the same device. For an example let’s think that a user has a session for a mobile application and he needs to use another web application by extending the user session without re-login. If we configure both these applications in WSO2 Identity Server, expecting the WSO2 Identity Server to act as the identity provider, we need to register both the web and mobile applications as service providers in WSO2 Identity Server.

Proposed solution.

diagram 1

According to above diagram(diagram 1) below are the steps for the proposed solution.

  1. Mobile application accesses the web application and it already has an access token received by using password grant type.

curl -k -v -H 'Authorization: Basic <base64Encoded clientId:secret>' -d "grant_type=mobile_password&username=<email/username>&password=<password>&scope=openid" -H "Content-Type:application/x-www-form-urlencoded" https://localhost:9443/oauth2/token

2. The mobile requests the access token(JWT) from Identity Server by submitting the existing access token it already has (refer step 1).

curl -i -X POST -H ‘Content-Type: application/x-www-form-urlencoded’ -H ‘Authorization: Basic <base64Encoded clientId:secret>’ -k -d ‘grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<Existing Token>’ https://localhost:9443/oauth2/token

3. The mobile service provider will generate the JWT bearer token and send it to the mobile application. This service provider will contain the public certificate to validate the token later.

4. Mobile will use a 3rd party application support to encrypt the access token and then include it in the authorized request to the web application.(it supports access tokens signed by “RSA256” algorithm)
Sample request is given below,

https://localhost:9443/oauth2/authorize?response_type=code&client_id=<web_sp_client_id>&scope=openid&redirect_uri=http://localhost:8080/sample_project/oauth2client&r=2-2-HWD&token=<Signed and Encrypted token>

5. Web application will receive the authorization request and inside the custom authenticator the token will be extracted from the request (refer above request in step 4).

6. Then the token will be decrypted by using the default private key in the primary key store and signature will be verified using the certificate in the mobile service provider. It will additionally check whether the token is available in the identity database as an additional security measure.

7. It will then set the authentication context for the user after checking the user’s existence in userstore and will also remove the validated token from the database to prevent it from being used again.

8. Then the user will be granted access to the web application and will be automatically logged in without prompting any credentials.

Implementation

According to the requirement and proposed solution we need to have two service providers for the mobile application and for the web application. The mobile application service provider should include a public certificate where we later use to validate the signature of the JWT token. This service provider should also generate JWT tokens. (refer [1] and [2])

SP certificate

Under “Inbound Authentication Configuration > OAuth/OpenIdConnect Configuration” add the “Audience Restriction” and the “Token Issuer”as below

According to the proposed solution when the user clicks on the web link from the mobile application it should by pass the login page and automatically logged in to the web application. To handle this auto login and token validation we need to have a custom authenticator (There can be other ways of implementing the same but according to the nature of the application we had, we thought of using a custom authenticator since there were some other business logic as well) (refer [4])

The web service provider is configured with OAuth/OpenIDConnect and it uses the Custom Authenticator for the authentication. (refer [3])

Inside the custom authenticator class override the following method in order to do the token verification and do the auto login of the user by setting the “AuthenticatorFlowStatus.SUCCESS_COMPLETED”. Example code is given below.

@Override
public AuthenticatorFlowStatus process(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException, LogoutFailedException {
String userName = null;
String decryptedToken;
//Super call to initialize properties
AuthenticatorFlowStatus status = super.process(request, response, context);
try {
if (TokenProcessorUtil.isTokenValid(request)) {
String token = request.getParameter("token");
try {
decryptedToken = TokenProcessorUtil.isEncrypted(token) ? TokenProcessorUtil.decrypt(token) : token;
JOSEObject jwtObject = JOSEObject.parse(decryptedToken);
userName = jwtObject.getPayload().toJSONObject().getAsString("sub");
} catch (ParseException | RequestObjectException e) {
log.error("Error in processing the JWT token: " , e);
return status;
}
try {
UserStoreManager userStoreManager = Commons.getUserStoreManager("SECOND_STORE",MultitenantConstants.SUPER_TENANT_DOMAIN_NAME);
if(!userStoreManager.isExistingUser(userName)){
return status;
}
} catch (UserStoreException e) {
log.error("Error obtaining the user from user store: " , e);
return status;
}
context.setSubject(AuthenticatedUser.createLocalAuthenticatedUserFromSubjectIdentifier(userName));
return AuthenticatorFlowStatus.SUCCESS_COMPLETED;
}
} catch (IdentityOAuth2Exception e) {
log.error("Error in processing the JWT token: " , e);
}
return status;
}

The following class contains the utility methods to do the token decryption and signature validation.

package com.custom.wso2.ciam.first.login;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObject;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.SignedJWT;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.json.JSONObject;
import org.wso2.carbon.identity.oauth.common.exception.InvalidOAuthClientException;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.RequestObjectException;
import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory;
import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters;
import org.wso2.carbon.identity.oauth2.token.OauthTokenIssuer;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
import org.wso2.carbon.identity.openidconnect.RequestParamRequestObjectBuilder;
import org.wso2.carbon.utils.multitenancy.MultitenantConstants;

import javax.servlet.http.HttpServletRequest;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;

import static org.wso2.carbon.identity.openidconnect.model.Constants.PS;
import static org.wso2.carbon.identity.openidconnect.model.Constants.RS;

public class TokenProcessorUtil {

private static final Log log = LogFactory.getLog(TokenProcessorUtil.class);

//Checks the signature of the JWT token
public static boolean isTokenValid(HttpServletRequest request) throws IdentityOAuth2Exception {
Certificate certificate;
String clientId;
String decryptedToken = null;
boolean isVerified = false;
String[] accessTokenArr = new String[1];
String accessTokenHash = null;
OauthTokenIssuer oauthTokenIssuer;
String token = request.getParameter("token");
if (StringUtils.isNotBlank(token)) {
if (isEncrypted(token)) {
try {
decryptedToken = decrypt(token);
} catch (RequestObjectException e) {
log.error("Error in decrypting the JWT token: " + e);
return false;
}
} else {
decryptedToken = token;
}
try {
SignedJWT signedJwt = SignedJWT.parse(decryptedToken);
JOSEObject jwtObject = JOSEObject.parse(decryptedToken);
String jsonStr = jwtObject.getPayload().toJSONObject().toJSONString();
JSONObject json = new JSONObject(jsonStr);
clientId = json.getJSONArray("aud").getString(0);
certificate = getX509CertOfOAuthApp(clientId, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME);

if (log.isDebugEnabled()) {
log.debug(new StringBuilder().append("Public certificate configured for Service Provider with ")
.append("client_id: ").append(clientId).append(" of tenantDomain: ")
.append(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)
.append(". Using public certificate for validating request object").toString());
}

isVerified = isSignatureVerified(signedJwt, certificate);

} catch (RequestObjectException | ParseException e) {
log.error("Error in processing JWT token: " + e);
return false;
}

try {
oauthTokenIssuer = OAuth2Util.getOAuthTokenIssuerForOAuthApp(clientId);
} catch (InvalidOAuthClientException e) {
log.error("Error in retrieving JWT token from database: " + e);
return false;
}
if (oauthTokenIssuer.usePersistedAccessTokenAlias()) {
try {
accessTokenHash = oauthTokenIssuer.getAccessTokenHash(decryptedToken);
} catch (OAuthSystemException e) {
log.error("Error in hashing JWT token: " + e);
return false;
}
}

accessTokenArr[0] = accessTokenHash;
String tokenId = OAuthTokenPersistenceFactory.getInstance().getAccessTokenDAO().getTokenIdByAccessToken(accessTokenHash);
if (StringUtils.isNotBlank(tokenId)) {
OAuthTokenPersistenceFactory.getInstance().getAccessTokenDAO().revokeAccessTokensIndividually(accessTokenArr, false);
} else {
isVerified = false;
}

}

return isVerified;
}

private static boolean isSignatureVerified(SignedJWT signedJWT, Certificate x509Certificate) {

JWSVerifier verifier;
if (x509Certificate == null) {
if (log.isDebugEnabled()) {
log.debug("No certificate found. ");
}
return false;
}

String alg = signedJWT.getHeader().getAlgorithm().getName();
if (log.isDebugEnabled()) {
log.debug("Signature Algorithm found in the JWT Header: " + alg);
}
if (alg.indexOf(RS) == 0 || alg.indexOf(PS) == 0) {
// At this point 'x509Certificate' will never be null.
PublicKey publicKey = x509Certificate.getPublicKey();
if (publicKey instanceof RSAPublicKey) {
verifier = new RSASSAVerifier((RSAPublicKey) publicKey);
} else {
if (log.isDebugEnabled()) {
log.debug("Public key is not an RSA public key.");
}
return false;
}
} else {
if (log.isDebugEnabled()) {
log.debug("Signature Algorithm not supported yet : " + alg);
}
return false;

}
// At this point 'verifier' will never be null;
try {
return signedJWT.verify(verifier);
} catch (JOSEException e) {
if (log.isDebugEnabled()) {
log.debug("Unable to verify the signature of the request object: " + signedJWT.serialize());
}
return false;
}
}

private static Certificate getX509CertOfOAuthApp(String clientId, String tenantDomain) throws RequestObjectException {

try {
return OAuth2Util.getX509CertOfOAuthApp(clientId, tenantDomain);
} catch (IdentityOAuth2Exception e) {
String errorMsg = "Error retrieving application certificate of OAuth app with client_id: " + clientId +
" , tenantDomain: " + tenantDomain;
if (StringUtils.isNotBlank(e.getMessage())) {
// We expect OAuth2Util.getX509CertOfOAuthApp() to throw an exception with a more specific reason for
// not being able to retrieve the X509 Cert of the service provider.
errorMsg = e.getMessage();
}
throw new RequestObjectException(errorMsg, e);
}
}

//Check whether the token is encrypted
public static boolean isEncrypted(String token) {
return token.split("\\.").length == 5;
}

//Decrypt the encrypted token
public static String decrypt(String token) throws RequestObjectException {
OAuth2Parameters params = new OAuth2Parameters();
params.setTenantDomain(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME);
return new RequestParamRequestObjectBuilder().decrypt(token, params);
}

}

Note : Above code samples are added just for the reference and guidance on the path to the implementation of the proposed solution. Depending on the scenario and the problem there can be variations. With above code it has more additional work to do before make it up and running. Please go through the references for additional information.

Enjoy coding…! :)

References

[1]-https://is.docs.wso2.com/en/5.10.0/learn/jwt-grant/
[2]-https://is.docs.wso2.com/en/5.10.0/learn/jwt-token-generation/
[3]-https://is.docs.wso2.com/en/5.10.0/learn/adding-and-configuring-a-service-provider/
[4]-https://is.docs.wso2.com/en/5.10.0/develop/writing-a-custom-local-authenticator/

--

--