WSO2 API Manager - Decode JWT in ESB using JWT Decode mediator.

Here in this post i will list code for JWTDecorder mediator which we can use in WSO2 ESB or any other synapse based WSO2 product to decode JWT header.

After message gone through this mediator, all claims in JWT will present in message context as properties. Property name will be claim name. So you can use them in rest of the mediation flow.

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.axiom.util.base64.Base64Utils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.oltu.oauth2.jwt.JWTException;
import org.apache.oltu.oauth2.jwt.JWTProcessor;
import org.apache.synapse.ManagedLifecycle;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseException;
import org.apache.synapse.SynapseLog;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.apache.synapse.mediators.AbstractMediator;
import org.wso2.carbon.context.CarbonContext;
import org.wso2.carbon.core.util.KeyStoreManager;



public class JWTDecoder extends AbstractMediator implements ManagedLifecycle {
   
    private static Log log = LogFactory.getLog(JWTDecoder.class);

    private final String CLAIM_URI = "http://wso2.org/claims/";
    private final String SCIM_CLAIM_URI = "urn:scim:schemas:core:1.0:";

    private KeyStore keyStore;

    public void init(SynapseEnvironment synapseEnvironment) {
        if (log.isInfoEnabled()) {
            log.info("Initializing JWTDecoder Mediator");
        }
        String keyStoreFile = "";
        String password = "";

        try {
            keyStore = KeyStore.getInstance("JKS");
        } catch (KeyStoreException e) {
            //throw new Exception("Unable to get JKS KeyStore instance");
        }
        char[] storePass = password.toCharArray();

        // load the key store from file system
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(keyStoreFile);
            keyStore.load(fileInputStream, storePass);
            fileInputStream.close();
        } catch (FileNotFoundException e) {
            if (log.isErrorEnabled()) {
                log.error("Error loading keystore", e);
            }
        } catch (NoSuchAlgorithmException e) {
            if (log.isErrorEnabled()) {
                log.error("Error loading keystore", e);
            }
        } catch (CertificateException e) {
            if (log.isErrorEnabled()) {
                log.error("Error loading keystore", e);
            }
        } catch (IOException e) {
            if (log.isErrorEnabled()) {
                log.error("Error loading keystore", e);
            }
        }
    }

    public boolean mediate(MessageContext synapseContext) {
        SynapseLog synLog = getLog(synapseContext);

        if (synLog.isTraceOrDebugEnabled()) {
            synLog.traceOrDebug("Start : JWTDecoder mediator");
            if (synLog.isTraceTraceEnabled()) {
                synLog.traceTrace("Message : " + synapseContext.getEnvelope());
            }
        }

        // Extract the HTTP headers and then extract the JWT from the HTTP Header map
        org.apache.axis2.context.MessageContext axis2MessageContext = ((Axis2MessageContext) synapseContext).getAxis2MessageContext();
        Object headerObj = axis2MessageContext.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
        @SuppressWarnings("unchecked")
        Map headers = (Map) headerObj;
        String jwt_assertion = (String) headers.get("x-jwt-assertion");

        // Incoming request does not contain the JWT assertion
        if (jwt_assertion == null || jwt_assertion == "") {
            // Since this is an unauthorized request, send the response back to client with 401 - Unauthorized error
            synapseContext.setTo(null);
            synapseContext.setResponse(true);
            axis2MessageContext.setProperty("HTTP_SC", "401");
            // Log the authentication failure
            String err = "JWT assertion not found in the message header";
            handleException(err, synapseContext);
            return false;
        }

        boolean isSignatureVerified = verifySignature(jwt_assertion, synapseContext);

        try {
            if (isSignatureVerified) {
                // Process the JWT, extract the values and set them to the Synapse environment
                if (log.isDebugEnabled()){
                    log.debug("JWT assertion is : "+jwt_assertion);
                }
                JWTProcessor processor = new JWTProcessor().process(jwt_assertion);
                Map claims = processor.getPayloadClaims();
                for (Map.Entry claimEntry : claims.entrySet()) {
                    // Extract the claims and set it in Synapse context
                    if (claimEntry.getKey().startsWith(CLAIM_URI)) {
                        String tempPropName = claimEntry.getKey().split(CLAIM_URI)[1];
                        synapseContext.setProperty(tempPropName, claimEntry.getValue());
                        if(log.isDebugEnabled()){
                            log.debug("Getting claim :"+tempPropName+" , " +claimEntry.getValue() );
                        }
                    } else if (claimEntry.getKey().startsWith(SCIM_CLAIM_URI)) {
                        String tempPropName = claimEntry.getKey().split(SCIM_CLAIM_URI)[1];
                        if (tempPropName.contains(".")) {
                            tempPropName = tempPropName.split("\\.")[1];
                        }

                        synapseContext.setProperty(tempPropName, claimEntry.getValue());
                        if(log.isDebugEnabled()){
                            log.debug("Getting claim :"+tempPropName+" , " +claimEntry.getValue() );
                        }
                    }
                }
            } else {
                return false;
            }
        } catch (JWTException e) {
            log.error(e.getMessage(), e);
            throw new SynapseException(e.getMessage(), e);
        }

        if (synLog.isTraceOrDebugEnabled()) {
            synLog.traceOrDebug("End : JWTDecoder mediator");
        }
       
        return true;
    }

    private boolean verifySignature(String jwt_assertion, MessageContext synapseContext) {
        boolean isVerified = false;
        String[] split_string = jwt_assertion.split("\\.");
        String base64EncodedHeader = split_string[0];
        String base64EncodedBody = split_string[1];
        String base64EncodedSignature = split_string[2];

        String decodedHeader = new String(Base64Utils.decode(base64EncodedHeader));
        byte[] decodedSignature = Base64Utils.decode(base64EncodedSignature);
        Pattern pattern = Pattern.compile("^[^:]*:[^:]*:[^:]*:\"(.+)\"}$");
        Matcher matcher = pattern.matcher(decodedHeader);
        String base64EncodedCertThumb = null;
        if (matcher.find()) {
            base64EncodedCertThumb = matcher.group(1);
        }
        byte[] decodedCertThumb = Base64Utils.decode(base64EncodedCertThumb);

        Certificate publicCert = null;


        publicCert = getSuperTenantPublicKey(decodedCertThumb, synapseContext);
        try {
            if (publicCert != null) {
                isVerified = verifySignature(publicCert, decodedSignature, base64EncodedHeader, base64EncodedBody,
                        base64EncodedSignature);
            } else if (!isVerified) {
                publicCert = getTenantPublicKey(decodedCertThumb, synapseContext);
                if (publicCert != null) {
                    isVerified = verifySignature(publicCert, decodedSignature, base64EncodedHeader, base64EncodedBody,
                            base64EncodedSignature);
                } else {
                    throw new Exception("Couldn't find a public certificate to verify signature");
                }

            }

        } catch (Exception e) {
            handleSigVerificationException(e, synapseContext);
        }
        return isVerified;
    }

    private Certificate getSuperTenantPublicKey(byte[] decodedCertThumb, MessageContext synapseContext){
        String alias = getAliasForX509CertThumb(keyStore, decodedCertThumb, synapseContext);
        if (alias != null) {
            // get the certificate associated with the given alias from
            // default keystore
            try {
                return keyStore.getCertificate(alias);
            } catch (KeyStoreException e) {
                if (log.isErrorEnabled()) {
                    log.error("Error when getting server public certificate: " , e);
                }
            }
        }
        return null;
    }

    private Certificate getTenantPublicKey(byte[] decodedCertThumb, MessageContext synapseContext){
        SynapseLog synLog = getLog(synapseContext);

        int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId();
        String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain();
       
        if (synLog.isTraceOrDebugEnabled()) {
            synLog.traceOrDebug("Tenant Domain: " + tenantDomain);
        }

        KeyStore tenantKeyStore = null;
        KeyStoreManager tenantKSM = KeyStoreManager.getInstance(tenantId);
        String ksName = tenantDomain.trim().replace(".", "-");
        String jksName = ksName + ".jks";
        try {
            tenantKeyStore = tenantKSM.getKeyStore(jksName);
        } catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error("Error getting keystore for " + tenantDomain, e);
            }
        }
        if (tenantKeyStore != null) {
            String alias = getAliasForX509CertThumb(tenantKeyStore, decodedCertThumb, synapseContext);
            if (alias != null) {
                // get the certificate associated with the given alias
                // from
                // tenant's keystore
                try {
                    return tenantKeyStore.getCertificate(alias);
                } catch (KeyStoreException e) {
                    if (log.isErrorEnabled()) {
                        log.error("Error when getting tenants public certificate: " + tenantDomain, e);
                    }
                }
            }
        }

        return null;
    }
   
    private boolean verifySignature(Certificate publicCert, byte[] decodedSignature, String base64EncodedHeader,
            String base64EncodedBody, String base64EncodedSignature) throws NoSuchAlgorithmException,
            InvalidKeyException, SignatureException {
        // create signature instance with signature algorithm and public cert,
        // to verify the signature.
        Signature verifySig = Signature.getInstance("SHA256withRSA");
        // init
        verifySig.initVerify(publicCert);
        // update signature with signature data.
        verifySig.update((base64EncodedHeader + "." + base64EncodedBody).getBytes());
        // do the verification
        return verifySig.verify(decodedSignature);
    }

    private String getAliasForX509CertThumb(KeyStore keyStore, byte[] thumb, MessageContext synapseContext) {
        SynapseLog synLog = getLog(synapseContext);
        Certificate cert = null;
        MessageDigest sha = null;

        try {
            sha = MessageDigest.getInstance("SHA-1");
        } catch (NoSuchAlgorithmException e) {
            handleSigVerificationException(e, synapseContext);
        }
        try {
            for (Enumeration e = keyStore.aliases(); e.hasMoreElements();) {
                String alias = e.nextElement();
                Certificate[] certs = keyStore.getCertificateChain(alias);
                if (certs == null || certs.length == 0) {
                    // no cert chain, so lets check if getCertificate gives us a result.
                    cert = keyStore.getCertificate(alias);
                    if (cert == null) {
                        return null;
                    }
                } else {
                    cert = certs[0];
                }
                if (!(cert instanceof X509Certificate)) {
                    continue;
                }
                sha.reset();
                try {
                    sha.update(cert.getEncoded());
                } catch (CertificateEncodingException e1) {
                    //throw new Exception("Error encoding certificate");
                }
                byte[] data = sha.digest();
                if (new String(thumb).equals(hexify(data))) {
                    if (synLog.isTraceOrDebugEnabled()) {
                        synLog.traceOrDebug("Found matching alias: " + alias);
                    }
                    return alias;
                }
            }
        } catch (KeyStoreException e) {
            if (log.isErrorEnabled()) {
                log.error("Error getting alias from keystore", e);
            }
        }
        return null;
    }

    private String hexify(byte bytes[]) {
        char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7',
                            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

        StringBuffer buf = new StringBuffer(bytes.length * 2);

        for (int i = 0; i < bytes.length; ++i) {
            buf.append(hexDigits[(bytes[i] & 0xf0) >> 4]);
            buf.append(hexDigits[bytes[i] & 0x0f]);
        }

        return buf.toString();
    }

    private void handleSigVerificationException(Exception e, MessageContext synapseContext) {
        synapseContext.setTo(null);
        synapseContext.setResponse(true);
        org.apache.axis2.context.MessageContext axis2MessageContext = ((Axis2MessageContext) synapseContext).getAxis2MessageContext();
        axis2MessageContext.setProperty("HTTP_SC", "401");
        String err = e.getMessage();
        handleException(err, synapseContext);
    }

    public void destroy() {
        if (log.isInfoEnabled()) {
            log.info("Destroying JWTDecoder Mediator");
        }
    }
}





Use OpenID Connect with OAuth 2.0 in WSO2 API Manager to retrieve user information

When we calling an API, we request for an oauth token which we send with each API call.

The reason for introduce OpenID on top of oauth 2.0 is oauth 2.0 access token retrieving process does not provide any additional information about user who is generating access tokens. Oauth is only authorization mechanism and we cannot derive more information from that.
So to get more information about user we will use openId scope to get user access token.

In that case we will pass openid scope with token request. Then we will get JWT as a part of the response from API manager, which contains user information in addition to access token and refresh token. So we will have all required user information after token generation process and we can use it for next steps if we need to do anything specific to users.
Issue following command to request OpenID based OAuth token.

Request
curl -k -d "grant_type=password&username=admin&password=admin&scope=openid" -H "Authorization: Basic M1J6RFNrRFI5ZmQ5czRqY296R2xfVjh0QU5JYTpXeElqSkFJd0dqRWVYOHdHZGFfcGM1Wl94RjRh, Content-Type: application/x-www-form-urlencoded" https://apiw.test.com/token

Response

{"scope":"openid","token_type":"Bearer","expires_in":3600,
"refresh_token":"65af3dbea3294b1524832d3869361e3e",
"id_token":"eyJhbGciOiJSUzI1NiJ9.eyJhdXRoX3RpbWUiOjE0MzA0NTY4MzM5OTgsImV4cCI6MTQzMDQ2MDQzNDAxNCwic3ViIjoiYWRtaW5AY2FyYm9uLnN1cGVyIiwiYXpwIjoiM1J6RFNrRFI5ZmQ5czRqY296R2xfVjh0QU5JYSIsImF0X2hhc2giOiJNV013WXpreVl6UmxPVGhsTkRNM01XTTVNVFEyTTJWbE0yWXlNamcwWXc9PSIsImF1ZCI6WyIzUnpEU2tEUjlmZDlzNGpjb3pHbF9WOHRBTklhIl0sImlzcyI6Imh0dHBzOlwvXC9sb2NhbGhvc3Q6OTQ0M1wvb2F1dGgyZW5kcG9pbnRzXC90b2tlbiIsImlhdCI6MTQzMDQ1NjgzNDAxNH0.Fc4DO8A22euo04vnBoE87RVBtDQ-73Z2hNZ8_WpeKslkumhEuUVcf6y03D5HZBlGDUi8zC1SUHewg4WEE8HvI6wA59wp8BErK6pY3Zb02pWbJsPh7VBHwky2g5PtvKSsGiy0rd2tuehY-_dAy7LBKNSUOhkmGdLXkSSThuIQxKOHDAJKHCY4I_36B9OH1scs34EG9MKG4vSNdfdcf4mSg0KUD98Jdw_NS-T4pRZK_sCeT-1BBodYEabEVREHxfcDr7BGYugMiiWThVUzd4WIHD83bVwxXP17POzuo6dS_l78pBWZtBBMPKXqhd9VMNZpc-sR07DS7KkHoV6Fp3l0oA",
"access_token":"1c0c92c4e98e4371c91463ee3f2284c"}

This response contain JWT as well. Then we can invoke user info API as follows.
curl -k -v -H "Authorization: Bearer 1c0c92c4e98e4371c91463ee3f2284c" https://km.test.com/oauth2/userinfo?schema=openid

HTTP/1.1 200 OK
{"email":"sanjeewa@wso2.com","family_name":"malalgoda"}As you see we can get results for user details.

Configure Categorization APIs for store via Tabs

If you want to see the APIs grouped according to different topics in the API Store, do the following:
Go to /repository/deployment/server/jaggeryapps/store/site/conf directory, open the site.json file and set the tagWiseMode attribute as true.
Go to the API Publisher and add tags with the suffix "-group" to APIs (e.g., Workflow APIs-group, Integration APIs-group, Quote APIs-group.)
Restart the server.
After you publish the APIs, you see the APIs listed under their groups. You can click on a group to check what the APIs are inside it.
If you want to have any other name for group you can do that as well. For that you need to change following property in site.json configuration file.
"tagGroupKey" :"-group",
Here -group can replace with any postfix you like.



WSO2 API Manager relationship between timestamp skew, token validity period and cache expiration time

Let me explain how time stamp skew works and how it effect to token generation.
First  time stamp skew is there to fix the issues due to small time differences in system clock values of servers. 
Let say you have 2 key managers and you generate token from one and authenticate with other. 
When first key manager generates token(say life span is 3600sec), time stamp skew value(say 300sec) will be deducted from token life time(client will notify that 3300sec is token validity period). 
Then he call to second key manager with that token exactly after 3200 secs and there is time different between key managers(second key manager has +300 sec time difference). 
In such cases time stamp skew will take care of those small gaps.

So theoretically 
time stamp skew should never large than token life time
It should be very small comparing to token validity period.
Token cache duration should never large than token validity period.

You can change configuration values according to requirements but you cannot put any random numbers as you need because those are inter related :-)

How to use cxf intercepter to pre-process requests to JAX-RS services - Apply security for JAX_RS services


When we use jax-rs services sometimes we need to add request pre processors to services. In this post i will discuss how we can use cxf interceptor in jax-rs services.
You may find more information from this url[http://cxf.apache.org/docs/interceptors.html]
package demo.jaxrs.server;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;

public class CustomOutInterceptor extends AbstractPhaseInterceptor {

    public CustomOutInterceptor() {
        //We will use PRE_INVOKE phase as we need to process message before hit actual service
        super(Phase.PRE_INVOKE );
    }

    public void handleMessage(Message outMessage) {
        System.out.println("Token: "+ ((TreeMap) outMessage.get(Message.PROTOCOL_HEADERS)).get("Authorization"));
  // Do your processing with Authorization transport header.
    }
}

Then we need to register Interceptor by adding entry to webapp/WEB-INF/cxf-servlet.xml file. Then it will execute before request dispatch to actual service.


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jaxrs="http://cxf.apache.org/jaxrs"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
         http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">
    <jaxrs:server id="APIService" address="/">
        <jaxrs:serviceBeans>
            <ref bean="serviceBean"/>
        </jaxrs:serviceBeans>

        <jaxrs:inInterceptors>
            <ref bean="testInterceptor"/>
        </jaxrs:inInterceptors>
    </jaxrs:server>
    <bean id="testInterceptor" class="demo.jaxrs.server.CustomOutInterceptor" />
    <bean id="serviceBean" class="demo.jaxrs.server.APIService"/>
</beans>




Then compile web app and deploy in application server. Once you send request with Authorization header you will noticed that it printed in server logs.

See following sample curl request
curl -k -v -H "Authorization: Bearer d5701a8ed6f677f215fa4d65c05e361" http://127.0.0.1:9763/APIManager/qqqq-1.0.0-admin/

And server logs for request
Token: [Bearer d5701a8ed6f677f215fa4d65c05e361]
API Service -- invoking getAPI, API id is: qqqq-1.0.0-admin

How to generate custom error message with custom http status code for throttled out messages in WSO2 API Manager.

In this post we will discuss how we can override the throttle out message HTTP status code. APIThrottleHandler.handleThrottleOut method indicates that the _throttle_out_handler.xml sequence is executed if it exists. If you need to send custom message with custom http status code we may execute additional sequence which can generate new error message. There we can override message body, http status code etc.

Create convert.xml with following content

<?xml version="1.0" encoding="UTF-8"?><sequence xmlns="http://ws.apache.org/ns/synapse" name="convert">
    <payloadFactory media-type="xml">
        <format>
            <am:fault xmlns:am="http://wso2.org/apimanager">
                <am:code>$1</am:code>
                <am:type>Status report</am:type>
                <am:message>Runtime Error</am:message>
                <am:description>$2</am:description>
            </am:fault>
        </format>
        <args>
            <arg evaluator="xml" expression="$ctx:ERROR_CODE"/>
            <arg evaluator="xml" expression="$ctx:ERROR_MESSAGE"/>
        </args>
    </payloadFactory>
    <property name="RESPONSE" value="true"/>
    <header name="To" action="remove"/>
    <property name="HTTP_SC" value="555" scope="axis2"/>
    <property name="NO_ENTITY_BODY" scope="axis2" action="remove"/>
    <property name="ContentType" scope="axis2" action="remove"/>
    <property name="Authorization" scope="transport" action="remove"/>
    <property name="Access-Control-Allow-Origin" value="*" scope="transport"/>
    <property name="Host" scope="transport" action="remove"/>
    <property name="Accept" scope="transport" action="remove"/>
    <property name="X-JWT-Assertion" scope="transport" action="remove"/>
    <property name="messageType" value="application/json" scope="axis2"/>
    <send/>
</sequence>


Then copy it to wso2am-1.6.0/repository/deployment/server/synapse-configs/default/sequences directory or use source view to add it to synapse configuration.
If it deployed properly you will see following message in system logs. Please check the logs and see is there any issue in deployment process.

[2015-04-13 09:17:38,885]  INFO - SequenceDeployer Sequence named 'convert' has been deployed from file : /home/sanjeewa/work/support/wso2am-1.6.0/repository/deployment/server/synapse-configs/default/sequences/convert.xml

Now sequence deployed properly then we may use it in _throttle_out_handler_ sequence. Add it as follows.

<?xml version="1.0" encoding="UTF-8"?><sequence xmlns="http://ws.apache.org/ns/synapse" name="_throttle_out_handler_">
    <sequence key="_build_"/>
    <property name="X-JWT-Assertion" scope="transport" action="remove"/>
    <sequence key="convert"/>
    <drop/>
</sequence>



Once _throttle_out_handler_ sequence deployed properly you will see following message in carbon logs. Check carbon console and see is there any errors with deployment.

[2015-04-13 09:22:40,106]  INFO - SequenceDeployer Sequence: _throttle_out_handler_ has been updated from the file: /home/sanjeewa/work/support/wso2am-1.6.0/repository/deployment/server/synapse-configs/default/sequences/_throttle_out_handler_.xml


Then try to invoke API until requests get throttled out. You will see following response.

curl -v -H "Authorization: Bearer 7f855a7d70aed820a78367c362385c86" http://127.0.0.1:8280/testam/sanjeewa/1.0.0


* About to connect() to 127.0.0.1 port 8280 (#0)
*   Trying 127.0.0.1...
* Adding handle: conn: 0x17a2db0
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x17a2db0) send_pipe: 1, recv_pipe: 0
* Connected to 127.0.0.1 (127.0.0.1) port 8280 (#0)
> GET /testam/sanjeewa/1.0.0 HTTP/1.1
> User-Agent: curl/7.32.0
> Host: 127.0.0.1:8280
> Accept: */*
> Authorization: Bearer 7f855a7d70aed820a78367c362385c86
>
< HTTP/1.1 555
< Access-Control-Allow-Origin: *
< Content-Type: application/json
< Date: Mon, 13 Apr 2015 05:30:12 GMT
* Server WSO2-PassThrough-HTTP is not blacklisted
< Server: WSO2-PassThrough-HTTP
< Transfer-Encoding: chunked
<
* Connection #0 to host 127.0.0.1 left intact
{"fault":{"code":"900800","type":"Status report","message":"Runtime Error","description":"Message throttled out"}}

How to use custom passowrd validation policy in WSO2 API Manager and update sign-up UI to handle new policy




In this article we will discuss how we can change password policy by using identity server specific configurations. Also we will discuss about updating API store sign-up page according to custom password policy. So when new users sign-up in API store it will be easy for them.


Please follow below instructions to change password policy by updating identity-mgt.properties file available in repository/conf/security directory.
You can find more information from this url - https://docs.wso2.com/display/IS450/Password+Policies

Do necessary changes in identity-mgt.properties file
# Identity listener is enable

Identity.Listener.Enable=true

# Define password policy enforce extensions

Password.policy.extensions.1=org.wso2.carbon.identity.mgt.policy.password.DefaultPasswordLengthPolicy
Password.policy.extensions.1.min.length=6
Password.policy.extensions.1.max.length=32
Password.policy.extensions.2=org.wso2.carbon.identity.mgt.policy.password.DefaultPasswordNamePolicy
Password.policy.extensions.3=org.wso2.carbon.identity.mgt.policy.password.DefaultPasswordPatternPolicy
Password.policy.extensions.3.pattern=^((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%&*])).{0,100}$
Password should contain a digit 0-9, a lower case letter a-z, an upper case letter A-Z, one of !@#$%&* characters as in Password.policy.extensions.3.pattern. But sign-up process has issue with this pattern.

Then we can customize sign-up process of store by adding sub theme.

1. create a folder call custom-signup in below path of store. You can give any preferable name.
wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/themes/fancy/subthemes/custom-signup

2. copy following folders to above location.
wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/themes/fancy/js folder to wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/themes/fancy/subthemes/custom-signup
wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/themes/fancy/templates folder to wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/themes/fancy/subthemes/custom-signup

3. Open below file. This file contain password validation function of sign-up.
wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/themes/fancy/subthemes/custom-signup/js/lib/jquery.validate.password.js
You can change validation logic written in $.validator.passwordRating = function(password, username) according to above pattern or whatever pattern you want.

Please find the attach jquery.validate.password.js modified according to above pattern.

(function($) {
       function rating(rate, message) {
        return {
            rate: rate,
            messageKey: message
        };
    }
    function uncapitalize(str) {
        return str.substring(0, 1).toLowerCase() + str.substring(1);
    }
    $.validator.passwordRating = function(password, username) {
            var minLength = 6;
            var passwordStrength   = 0;
            if ((password.length >0) && (password.length <=5)) passwordStrength=0;
            if (password.length >= minLength) passwordStrength++;
            if ((password.match(/[a-z]/)) && (password.match(/[A-Z]/))) passwordStrength++;
            if (password.match(/\d+/)) passwordStrength++;
            if (password.match(/.[!,@,#,$,%,^,&,*]/)) passwordStrength++;
            if (password.length > 12) passwordStrength++;
            if (username && password.toLowerCase()== username.toLowerCase()){
                passwordStrength = 0;
            }
            $('#pwdMeter').removeClass();
            $('#pwdMeter').addClass('neutral');
            switch(passwordStrength){
            case 1:
                return rating(1, "very-weak");
              break;
            case 2:
                return rating(2, "weak");
              break;
            case 3:
                return rating(3, "weak");
              break;
            case 4:
                return rating(4, "medium");
              break;
            case 5:
                 return rating(5, "strong");
              break;
            case 6:
                 return rating(5, "vstrong");
              break;
            default:
                return rating(1, "very-weak");
            }
    }
    $.validator.passwordRating.messages = {
        "similar-to-username": "Too similar to username",
        "very-weak": "Very weak",
        "weak": "Weak",
        "medium": "Medium",
        "strong": "Strong",
        "vstrong": "Very Strong"
    }
    $.validator.addMethod("password", function(value, element, usernameField) {
        // use untrimmed value
        var password = element.value,
        // get username for comparison, if specified
            username = $(typeof usernameField != "boolean" ? usernameField : []);        
        var rating = $.validator.passwordRating(password, username.val());
        // update message for this field
        var meter = $(".password-meter", element.form);
        meter.find(".password-meter-bar").removeClass().addClass("password-meter-bar").addClass("password-meter-" + rating.messageKey);
        meter.find(".password-meter-message")
        .removeClass()
        .addClass("password-meter-message")
        .addClass("password-meter-message-" + rating.messageKey)
        .text($.validator.passwordRating.messages[rating.messageKey]);
        // display process bar instead of error message
        return rating.rate > 3;
    }, "Minimum system requirements not met");
    // manually add class rule, to make username param optional
    $.validator.classRuleSettings.password = { password: true };
   
})(jQuery);


Also make sure return rating.rate > 2; of $.validator.addMethod("password", function(value, element, usernameField) will give you the expected strength of password.
We can modify it as follows
return rating.rate > 3;
to make sure that entered password adhere above pattern.


4. If you want to change above Password.policy.extensions.3.pattern to different pattern and show password tips to user when entering password you need to edit following files.
wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/themes/fancy/subthemes/custom-signup/templates/user/sign-up/template.jag

Below section correspond to display password tips
  
<div class="help-block" id="password-help" style="display:none">
 <%=i18n.localize("pwHelpMsgLine1")%>
 <ul>
 <li>This is new changed TIP :-) </li>                
<li><%=i18n.localize("pwHelpMsgLine3")%></li> 
<li><%=i18n.localize("pwHelpMsgLine4")%></li>
<li><%=i18n.localize("pwHelpMsgLine5")%></li>
</ul>
</div>
                 
  
Please change text as you want in below file accroding to above keys.
wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/conf/locales/jaggery/locale_default.json


5. Finally, do below config to enable above changes in store.
Open wso2am-1.7.0/repository/deployment/server/jaggeryapps/store/site/conf/site.json
Add sub theme folder as below
    "theme" : {
        "base" : "fancy",
        "subtheme" : "custom-signup"
    },
  
This will override current user sign-up validation by above changes as well as if you did any changes to password tips text.

Empowering the Future of API Management: Unveiling the Journey of WSO2 API Platform for Kubernetes (APK) Project and the Anticipated Alpha Release

  Introduction In the ever-evolving realm of API management, our journey embarked on the APK project eight months ago, and now, with great a...