Forum Discussion

srixon's avatar
srixon
Occasional Contributor
2 years ago
Solved

How to handle when there is a separate Basic Auth endpoint and api endpoint for each environment

How to handle when there is a separate Basic Auth endpoint that returns a JWT and api endpoint for each environment

 

Here is the use case:

1.  There are multiple environments and each environment has a tenant. 

Example <env1><tenant1>, <env1><tenant2>, env2><tenant1>

2. Each environment has an auth endpoint and api endpoint.  Example:

Auth endpoint:   https://<env1><tenant1>auth.com

API endponts:  https://<env1><tenant1>api.com/api

3.  The auth endpoint is Basic Authentication and requires a username with this structure:

username:  <env1><tenant1>/username

password:  ***********

4.  The auth endpoint returns a JWT token, in the following response

{
    "access_token": "JWT",
    "profile_id": "",
    "token_type": "Bearer",
    "expires_in": 86400
}

5.  The access_token must be parsed from the response and then included in the Header of the api requests. 

 

In it's simplest form, I was able to accomplish this by using:

- No Environment

- Hardcoding the Auth endpoint

- Hardcoding the api endpoint

- Getting the token in an auth request 'Login' test step

- Grabbing the token and using it in the next step by placing this in the header:  Bearer ${Login#Response#$['access_token']}

 

But I need to be able to automate this so that I can switch environments at run time.

 

I've done a lot of research and haven't found many suggestions besides 'create a script'.  I'm hoping there are other ways, but if I have to go the script route, it's not the end of the world.  Has anyone else encountered a similar scenario?  What is the easier strategy for handling a scenario like this?  I appreciate any advice!

 

NOTE:  I can't seem to use the built in Authentication Manger in ReadyAPI because the Basic Authentication returns a response that must be parsed.  Unless there is a way around this that I don't know about 🙂

  • srixon ,

    given the details you gave us above I would suggest the following solution:

    • Set your tenant as the project level property, e.g. tenant
    • Define environment specific stuff within Environment configuration
      • Define two APIs:
        • one for authorization API
        • second for your target API
      • In the endpoints use project-level tenant property (e.g. ${#Project#tenant}) to parametrize the endpoints
      • Grab the token from the authorization response and save it to another project-level property

    When executing the testrunner, use parameters:

    -E to set the environment

    -P to set the tenant (project level variable)

     

    Best regards,

    Karel

     

10 Replies

  • KarelHusa's avatar
    KarelHusa
    Champion Level 3

    srixon ,

    given the details you gave us above I would suggest the following solution:

    • Set your tenant as the project level property, e.g. tenant
    • Define environment specific stuff within Environment configuration
      • Define two APIs:
        • one for authorization API
        • second for your target API
      • In the endpoints use project-level tenant property (e.g. ${#Project#tenant}) to parametrize the endpoints
      • Grab the token from the authorization response and save it to another project-level property

    When executing the testrunner, use parameters:

    -E to set the environment

    -P to set the tenant (project level variable)

     

    Best regards,

    Karel

     

    • srixon's avatar
      srixon
      Occasional Contributor

      Hi Karel,

       

      Thank you for the suggestions. 

      • I tested adding the parameters to the endpoints and that is working great.
      • I also defined two APIs as you suggested

      However, in regards to your last bullet "Grab the token from the authorization response and save it to another project-level property".  At what point do you suggest I do this?  Should this be done in a script that runs at the beginning of the project?  I assume yes, and if so, are there any good script examples?  Hoping that ReadyAPI already has some methods I can use so I do not have to reinvent the wheel. 

       

       

      • KarelHusa's avatar
        KarelHusa
        Champion Level 3

        srixon ,

        the solution depends on the structure of your tests and also on the authentication methods.

        If you can satisfy the authentication with any method from Auth manager, that would be the easiest way to go.

         

        Otherwise you can e.g. dedicate a test case for authentication. In such test case you can invoke the authentication, process its response and save into a property. You can avoid scripting if you use property transfer test step. Also you can use a Run Test Case step to invoke it wherever needed.

         

        Maybe you can close this thread (which relates to endpoint configuration) and start a new one for authentication if needed.

         

        Best regards,

        Karel

         

  • richie's avatar
    richie
    Community Hero
    Hey srixon,

    When they introduced the environments functionality several years ago i tried using it, but struggled (i think the functionality has matured since) and nmrao mentioned an approach to handling multiple environments that i've used ever since.


    I create a properties file (containing custom project level properties) for each different environment.
    Within this properties file i have all my environment specific static properties (like hostnames, urls, usernames/passwords, db connection string variables, static tokens, etc.) and i have empty properties for my dynamic properties (like GUIDs, dynamic token values, etc.)

    I load my properties file in before execution.
    At this point my project level custom properties and their static values and the properties with empty are created/updated.
    I then have a "setup testsuite" that essentially queries the environment populating the empty dynamic properties with values. Virtually all my environment specfic properties are populated by various different tests in my setup testsuite.
    For stuff that expires quickly (like JWT's), i do have empty project properties setup, but i generate those from within the individual tests itself.

    Essentially i parameterise every piece of environment specific data and source the data values from the custom project properties (either generated when i load in the properties file (static data), or in my setup testsuite (dynamic data that doesnt expire).

    Since i started using this approach i havent used any other to handle changing environments.

    All credit goes to Rao for this idea....it really has helped me for the last couple of years.

    Cheers,

    Rich
    • srixon's avatar
      srixon
      Occasional Contributor

      Hi richie,

       

      Thanks for the suggestions.   At my previous company we set properties in files, but I was hoping I could do something like what Karel suggested using the -E and -P and set properties at run time.  Then I don't have to update or store files.  I will consider this option, but will have to test it.

       

      I'm interested in your setup suite.  How do you have this configured so it always runs first?

      • richie's avatar
        richie
        Community Hero

        Hey srixon 

         

        to answer your question - I have my setup testsuites disabled, and then I have a 'Setup' testcase with a 'Run Test Case' step in each of my Functional testsuites.  The 'Run Test Case' step executes the 'Setup' testcases within the disabled 'Setup' testsuites ( these 'Setup' testsuites are always listed in my project BEFORE the Functional testsuites.

         

         

        Please see image below - these 'disabled' testsuites are my 'Setup' testsuites.  Each one below contains a testcase that does something different:

         

         

         the 'Generate CRM Access Token' testsuite generates the token needed for later functionaltests

        The 'Generate GUIDs' testsuite queries the environment for the GUIDs needed later in the functional tests (these are some of the empty project level properties)

        The 'Identiy Test Data Record' testsuite queries the environment for a specific testdata record needed in later tests

        The 'Generate Java JWT' generates the token I need for later tests (again, this populates an empty project level property)

         

         

         

         

        My Functional TestSuites look like the following:

         

        So in the image above - SYSLNC001.017 is a functional test suite.

        It contains a 'Setup' testcase with 2 teststeps - both of these teststeps are 'Run Test Case' steps which point to the relevant testcases within the disabled 'Setup' testsuites further up in the project hierarchy.

        The 017-1 testcase is my functional testcase within the SYSLNC001.017 functional testsuite.

         

        As I already mentioned - the way I do it is that I have a environmentName_properties file which is just a list of name value pairs like the following:

         

         

         

        Generate CRM Access Token=eyJ0eXAiOiJ
        Exports Token Value=eyJ0eXAiOiJKV1
        Exports_CM=https://management-integration.azurewebsites.net/
        Exports_Auth=https://authentication-integration.azurewebsites.net/
        CRM=https://defra-cus.crm4.dynamics.com/
        defraexp_Organisation@odata.bind=
        defraexp_ApplicantId@odata.bind=
        defraexp_CertifierOrganisation@odata.bind=
        resource=https://.crm4.dynamics.com
        client_id=2fcc6119-c8a0-44a3-8c12-
        client_secret=Z%2AePjVgTc2U_
        grant_type=client_credentials
        EXP_resource=e621ab05-3417-448
        EXP_client_id=e621ab05-3417-448
        EXP_client_secret=a%2BOPsfWybHR
        EXP_username=exportsadministrator@sandpit.onmicrosoft.com
        EXP_password=Rojo5166!
        EXP_grant_type=password
        LogicApp=https://northeurope.logic.azure.com:443/
        LogicApp2=https://northeurope.logic.azure.com:443/workflows/triggers/manualp-GpmOmxvBLuMWcYD3mhk
        DevEndpoint=https://dev.azure.defra.cloud
        TestEndpoint=https://tst.azure.defra.cloud
        X-Api-Key=WNvLMgN{`,&
        X-Api-Client=TESTERS
        DB_Host=127.0.0.1
        DB_Port=1433
        DB_User=sa
        DB_Database=Reference

         

         

         

         

        Now - the above list (I've edited it - you can see some static parameters - where there is an attribute separated with it's associated value by the equals sign (e.g. bottom row has 'DB_Database=Reference') and I have some empty properties like the line 'defraexp_ApplicantId@odata.bind='

         

        I populate the 'defraexp_ApplicantId@odata.bind=' property by executing one of the Setup Testsuites.

         

        The way I always do this is to open ReadyAPI, and then load in the envirnmentName.properties file in manually. HOWEVER - if you want to do switch environments WITHOUT bothering launching ReadyAPI, what I have done on some occasion is to launch the testRunner, then paste the content of my properties file into the relevant Project level Properties editable field - see following image:

         

         

        As you can see - I've pasted all my the content from my properties file into the field.

         

        At this point, you can click 'Get Command Line' and this will generate the command with all those property settings built in.  I then took a copy of the testRunner generated line with all the property settings and saved them in a file.

         

        When I did this before - I repeated this process for each of my different environments. - each time I had a new environment, I

        1. built up the content of my properties file, then once it was built - I then

        2. copied the content into the Project level Properties editable field in the TestRunner form above,

        3. selected 'Get Command Line' and copied and saved the content to a separate file.

         

        Eventually i had like maybe 5 txt files that contained the content for the testRunner command for each environment.

         

         

        Then - each time I wanted to execute my project with a specific environment - I had the correct TestRunner command with all the properties set (both static and dynamic).  As I said - for the empty dynamic properties - these are all populated by specific setup testsuites querying the environment when needed via the 'Run TestCase' teststeps in 'setup' testcases within my Functional Testsuites.

         

        Does that help explain the way I do it? 

         

        there's bound to be a number of options available - but the above setup is the one i prefer using (probably cos I'm used to it now).  The advantage I've found by doing it this way is if you plan your project out correctly, essentially you can build all your ReadyAPI projects so you can lift and shift them to run on any different environment WITHOUT needing to make ANY changes to the testcontent in the project.

         

        I do this with all my ReadyAPI projects now - it's very efficient!

         

        Cheers,

         

        Rich

  • srixon's avatar
    srixon
    Occasional Contributor

    I ended up using project-level tenant property (e.g. ${#Project#tenant}) to parametrize the endpoints as suggested by KarelHusa Thank you!

     

    If anyone is interested, I also created a groovy script to grab and parse the token.  Note, I'm a beginner at groovy script, so there may be an easier way to do this, but it worked for me 🙂

     

    /****
    @author: Scott Rixon

    Class to get the bearer token from the oauth/accessToken endpoint

    ****/

    import org.apache.http.HttpHost;
    import org.apache.http.client.methods.HttpGet;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import org.apache.http.auth.AuthScope;
    import org.apache.http.auth.UsernamePasswordCredentials;
    import org.apache.http.client.CredentialsProvider;
    import org.apache.http.impl.client.BasicCredentialsProvider;
    import org.apache.http.client.AuthCache
    import org.apache.http.impl.client.BasicAuthCache
    import org.apache.http.impl.auth.BasicScheme
    import org.apache.http.client.protocol.HttpClientContext
    import org.apache.http.client.methods.CloseableHttpResponse
    import org.apache.http.HttpEntity
    import org.apache.http.StatusLine
    import org.apache.http.client.HttpResponseException
    import org.apache.http.client.ClientProtocolException
    import org.apache.http.util.EntityUtils
    import groovy.json.JsonSlurper

     

    public class Bearer_Token
    {
    String env
    String username
    String password
    String token_type
    String access_token
    String url
    String uriPath
    String response
    String fullUrl
    String host

    public Bearer_Token(String env){
    env = env
    username = ""
    password = ""
    host = ""
    url = "https://${host}"
    uriPath = "/oauth/accessToken"
    fullUrl = "${url}${uriPath}"

    }

    public send_request(log){

     

    // Create Client
    CloseableHttpClient httpclient = HttpClients.createDefault();

     

    // Create Host
    HttpHost targetHost = new HttpHost(host, 443, "https");

     

    // Create Credential provider
    CredentialsProvider credentialsPovider = new BasicCredentialsProvider();

     

    // Set Credentials
    credentialsPovider.setCredentials(
    new AuthScope(targetHost.getHostName(), targetHost.getPort()),
    new UsernamePasswordCredentials(username, password));

     

    // Create AuthCache instance
    AuthCache authCache = new BasicAuthCache();

    // Generate BASIC scheme object and add it to the local auth cache
    BasicScheme basicAuth = new BasicScheme();
    authCache.put(targetHost, basicAuth);

     

    // Add AuthCache to the execution context
    HttpClientContext context = HttpClientContext.create();
    context.setCredentialsProvider(credentialsPovider);
    context.setAuthCache(authCache);

     

    // Create HTTPGet
    HttpGet httpget = new HttpGet(fullUrl);


    // Add headers
    httpget.addHeader("Accept", "application/json");
    httpget.addHeader("Content-Type", "application/json");
    httpget.addHeader("Accept-Charset", "utf-8");

     

    // Execute command
    CloseableHttpResponse response = httpclient.execute(
    targetHost, httpget, context);

     

    // Get Status
    StatusLine statusLine = response.getStatusLine()

     

    // Get Entity
    HttpEntity entity = response.getEntity();

     

    // Throw exception if response code isn't successful
    if (statusLine.getStatusCode() >= 300) {
    throw new HttpResponseException(
    statusLine.getStatusCode(),
    statusLine.getReasonPhrase());
    }
    if (entity == null) {
    throw new ClientProtocolException("Response contains no content");
    }

    String content = EntityUtils.toString(entity);

     

    // Parse JSON
    def jsonSlurper = new JsonSlurper()
    def object = jsonSlurper.parseText(content)
    def jsonResonse = "${object['token_type']} ${object['access_token']}"

    log.info(jsonResonse)

    response.close();

    return jsonResonse

    }

    }

     

    // Get Environment
    String node = project.getPropertyValue("node")
    String tenant = project.getPropertyValue("tenant")

     

    if(!node){
    log.error("No node is configured in the Project Properties")
    }

     

    if(!tenant){
    log.error("No tenant is configured in the Project Properties")
    }

     

    // Create Environment string
    String env = "${node}${tenant}"
    log.info("The environment is: ${env}")

     

    // Get Bearer token
    def response
    def t = new Bearer_Token(env)
    try{
    response = t.send_request(log)
    }
    catch(err){
    log.error(err)
    }

     

    // Set bearer token
    project.setPropertyValue("bearer_token", response)

    • richie's avatar
      richie
      Community Hero

      Hey srixon 

       

      Thanks man - the groovy script looks handy - so I'll be stealing that - thank you! 🙂

       

      Cheers,

       

      Rich