Tuesday, May 19, 2015

How to use Authorization code grant type (Oauth 2.0) with WSO2 API Manager 1.8.0

1. Create API in WSO2 API Manager publisher and create application in API store. When you create application give some call back url as follows. http://localhost:9764/playground2/oauth2client
Since i'm running playground2 application in application server with port offset 1 i used above address. But you are free to use any url.

2. Paste the following on browser - set your value for client_id

Sample command
curl -v -X POST --basic -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "client_id=YOUR_CLIENT_ID&grant_type=authorization_code&code=YOUR_AUTHORIZATION_CODE&redirect_uri=https://localhost/callback" https://localhost:9443/oauth2/token

Exact command:
http://localhost:8280/authorize?response_type=code&scope=PRODUCTION&client_id=O2OkOAfBQlicQeq5ERgE7Wh4zeka&redirect_uri=http://localhost:9764/playground2/oauth2client

3. Then it will return something like this. Copy the authorization code from:
Response from step 02:
http://localhost:9764/playground2/oauth2client?code=e1934548d0a0883dd5734e24412310

4. Get the access token and ID token from following

Sample command:
curl -v -X POST --basic -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "client_id=YOUR_CLIENT_ID&grant_type=authorization_code&code=YOUR_AUTHORIZATION_CODE&redirect_uri=https://localhost/callback" https://localhost:9443/oauth2/token

Exact command:
curl -v -X POST --basic -u O2OkOAfBQlicQeq5ERgE7Wh4zeka:Eke1MtuQCHj1dhM6jKsIdxsqR7Ea -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "client_id=O2OkOAfBQlicQeq5ERgE7Wh4zeka&grant_type=authorization_code&code=e1934548d0a0883dd5734e24412310&redirect_uri=http://localhost:9764/playground2/oauth2client" http://localhost:8280/token

Response from step 04:
{"scope":"default","token_type":"bearer","expires_in":3600,
"refresh_token":"a0d9c7c4f96baed42da2c167e1ebbb75","access_token":"2de7da7e3822cf75fd7983cfe1337ec"}

5. Now call your API with the access token from step-4

curl -k -H "Authorization: Bearer 2de7da7e3822cf75fd7983cfe1337ec"
http://10.100.1.65:8280/test-api/1.0.0

Monday, May 11, 2015

How to add custom message to WSO2 API Publisher - Show custom message based on user input or selections

In API Manager we can add sub themes and change look and feel of jaggery applications.
Please follow below instructions. Here i have listed instructions to add new warning message when you changed API visibility.

(1) Navigate to "/repository/deployment/server/jaggeryapps/publisher/site/themes/default/subthemes" directory.
(2) Create a directory with the name of your sub theme. For example "new-theme".
(3) Copy /repository/deployment/server/jaggeryapps/publisher/site/themes/default/templates/item-design/template.jag to the new subtheme location "/repository/deployment/server/jaggeryapps/publisher/site/themes/default/subthemes/new-theme/templates/item-design/template.jag".

(4) Go to template.jag file in sub themes directory we created and find following code block.

        $('#visibility').change(function(){
            var visibility = $('#visibility').find(":selected").val();
            if (visibility == "public" || visibility == "private" || visibility == "controlled"){
                $('#rolesDiv').hide();
            } else{
                $('#rolesDiv').show();
            }
        });
Then change it as follows. You can change message if need.

        $('#visibility').change(function(){
            var visibility = $('#visibility').find(":selected").val();

            if (visibility != "public"){
            jagg.message({content:"You have changed visibility of API, so based on visibility subscription availability may change",type:"warn"});
            }

            if (visibility == "public" || visibility == "private" || visibility == "controlled"){
                $('#rolesDiv').hide();
            } else{
                $('#rolesDiv').show();
            }
        });



(5) Edit "/repository/deployment/server/jaggeryapps/publisher/site/conf/site.json" file as below in order to make the new sub theme as the default theme.
        "theme" : {
               "base" : "fancy",
                "subtheme" : "new-theme"
        }



Here i have listed generic instructions to change this using sub themes.

Update key stores in WSO2 API Manager and Identity server(production recommendation )


Here in this post i will discuss how we can generate keystores and include them to WSO2 products before they deploy in production deployment.

Most of organizations have their own crt file and keys. So lets see how we can use them to create certificates required for WSO2 servers and deploy them.

Step 1: Follow follow steps to update the keystore.

openssl pkcs12 -export -in /etc/pki/tls/certs/sanjeewa-com.crt -inkey /etc/pki/tls/private/private-key.key -name "wso2sanjeewa" -certfile /etc/pki/tls/certs/DigiCertCA.crt -out wso2sanjeewa.pfx
Enter Export Password:
Verifying - Enter Export Password:

/usr/java/default/bin/keytool -importkeystore -srckeystore wso2sanjeewa.pfx -srcstoretype pkcs12 -destkeystore wso2sanjeewa.jks -deststoretype JKS
Enter destination keystore password: 
Re-enter new password:
Enter source keystore password: 
Entry for alias wso2sanjeewa successfully imported.
Import command completed:  1 entries successfully imported, 0 entries failed or cancelled

 /usr/java/default/bin/keytool -export -alias wso2sanjeewa -keystore wso2sanjeewa.jks -file wso2sanjeewa.pem
Enter keystore password: 
Certificate stored in file

/usr/java/default/bin/keytool -import -alias wso2sanjeewa -file /opt/o2/wso2sanjeewa.pem -keystore client-truststore.jks -storepass wso2carbon
Certificate was added to keystore

Now we have all files we need to copy.

Step 2: We need to copy them to /opt/o2/WSO2Servers/wso2am-1.8.0/repository/resources/security
This file path change with product you use(normally its /repository/resources/security ).
We need to do this for all products deployed in this deployment.

Step 3: Then after that you need to find all occurrence of wso2carbon.jks( grep -rnw 'wso2carbon.jks') and replace them with wso2sanjeewa.jks file we generated in above steps.

Step 4:Then search for alias(grep -rnw 'alias') and KeyAlias(grep -rnw 'KeyAlias') in all files in wso2Server. If you found them as wso2carbon then only replace it with wso2sanjeewa.

We need to follow these steps very carefully. Here we have listed filenames and changed parameters for your reference.

While you carry out step 3 and 4 for IS you need to change following files and lines

repository/conf/security/application-authentication.xml:96:         
<Parameter name="TrustStorePath">/repository/resources/security/wso2sanjeewa.jks</Parameter>

repository/conf/identity.xml:244:               
<Location>${carbon.home}/repository/resources/security/wso2sanjeewa.jks</Location>

repository/conf/carbon.xml:302:            
<Location>${carbon.home}/repository/resources/security/wso2sanjeewa.jks</Location>

repository/conf/carbon.xml:308:            
<KeyAlias>wso2sanjeewa</KeyAlias>

repository/conf/carbon.xml:318:            
<Location>${carbon.home}/repository/resources/security/wso2sanjeewa.jks</Location>

repository/conf/carbon.xml:324:           
<KeyAlias>wso2sanjeewa</KeyAlias>




While you carry out step 3 and 4 for APIM you need to change following files and lines

repository/deployment/server/jaggeryapps/store/site/conf/site.json:14:       
"identityAlias" : "wso2sanjeewa",

repository/deployment/server/jaggeryapps/store/site/conf/site.json:16:        
"keyStoreName" :"/opt/o2/WSO2Servers/wso2am-1.8.0/repository/resources/security/wso2sanjeewa.jks"

repository/deployment/server/jaggeryapps/publisher/site/conf/site.json:14:        
"identityAlias" : "wso2sanjeewa",

repository/deployment/server/jaggeryapps/publisher/site/conf/site.json:16:        
"keyStoreName" :"/opt/o2/WSO2Servers/wso2am-1.8.0/repository/resources/security/wso2sanjeewa.jks"

repository/conf/security/secret-conf.properties:21:
keystore.identity.location=repository/resources/security/wso2sanjeewa.jks

repository/conf/security/secret-conf.properties:23:
keystore.identity.alias=wso2sanjeewa

repository/conf/security/secret-conf.properties:32:
keystore.trust.alias=wso2sanjeewa

repository/conf/carbon.xml:313:            
<Location>${carbon.home}/repository/resources/security/wso2sanjeewa.jks</Location>

repository/conf/carbon.xml:319:            
<KeyAlias>wso2sanjeewa</KeyAlias>

repository/conf/carbon.xml:329:            
<Location>${carbon.home}/repository/resources/security/wso2sanjeewa.jks</Location>

repository/conf/carbon.xml:335:            
<KeyAlias>wso2sanjeewa</KeyAlias>



Friday, May 8, 2015

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");
        }
    }
}





Wednesday, May 6, 2015

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.