Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to add and update environment variables, modify headers, and new lifecycle events #2121

Open
1 of 3 tasks
ortonomy opened this issue Feb 4, 2023 · 16 comments
Open
1 of 3 tasks

Comments

@ortonomy
Copy link

ortonomy commented Feb 4, 2023

Is your feature request related to a problem? Please describe.

I have an AWS AppSync graphql endpoint. It's secured by Cognito (the auth system for AWS.). It requires an ID token to be able to authenticate. I can't automatically use a valid token, or generate a new one before every request like I can with REST in say, Postmans.

Describe the solution you'd like

I'm trying to write a plugin for Altair that will allow me to manage login and credentials to an AWS Cognito Pool using confidential-client credentials (username,password,client secret, region, poolId) using the aws-sdk lib

I wanted to write my plugin such that it could satisfy:

  1. Pre-request - check local storage for an existing ID/Access token
  2. Check its validity based on expiry
  3. Renew the access/ID token if it was expired
  4. store the token in local storage
  5. use the token in Authorization header to authorize all the graphql requests including fetching the schema

we need:

  • a pre-request hook for plugins (so this can be global)
  • a context API for plugins that allows setting env, and reading env, and setting headers

Describe alternatives you've considered
A pre-request script - however, I can't use node packages here and the available modules are too limited (for example decoding a JWT) to do the proper logic needed

Additional context
N/A

Tasks

Preview Give feedback
@imolorhe
Copy link
Collaborator

imolorhe commented Feb 4, 2023

Hey! Trying to make sure I understand the issue here

I can't automatically use a valid token, or generate a new one before every request like I can with REST in say, Postmans.

Why can't you automatically use a valid token? Why can't you generate a new one before every request?
What is missing in the pre-requests that doesn't allow you do that?

@ortonomy
Copy link
Author

ortonomy commented Feb 5, 2023

Hope I can give a comprehensive answer:

Why can't you automatically use a valid token?

Because I can't set headers from the pre-request script. For some reason I'm given access to the custom headers which have already been set in the array on altair.data.headers but if I push a new value, no extra headers are sent...

So I have to create an empty ENV, and pre-emptively set a the header referencing the empty ENV and then populate it from the script. I don't consider this automated.

Why can't you generate a new one before every request?

Yes, I concede, I can do this. But seems expensive and unnecessary.

What is missing in the pre-requests that doesn't allow you do that?

Here's what I want to do:

  • create a side bar action button with a custom icon
  • this brings up a panel in which I can input all the AWS client info
  • also select the environment (Dev, Staging, whatever I'm saving these values for)
  • save these values to the selected environment
  • before every request (including schema fetching) run a script with these saved values and retrieve any cached AWS credentials, check if they've expired, and fetch or refresh if necessary
  • automatically set the Auth headers

I would like to make this a plugin I can share with others including my teammates, so that we have a robust and easy to use GraphQL client. Insomnia just broke their plugin system with the latest updates, and Postman is rubbish for GraphQL.

The pre-requests script at worst needs to be repeated per request, and at best per collection (I split my collections by Query/Mutations and by app)

Top issue:

  • I can't modify the headers even in pre-request-scripts. Have to pre set up the header bound to a blank env
  • the pre-request scripts are manual, and don't lend themselves to sharing
  • the pre-request script requires setting up a whole bunch of other ENVs to support the log in input
  • pre-request scripts don't let me set up a custom UI to collect all the info
  • I can't easily switch off the authentication
  • The plugin system allows me to use a full node execution environment so I can include libs like aws-sdk and jwt-decode to make my life easier - see the script below. It's quite verbose just to do AWS auth, and it could be much shorter if I could load the SDK client and just authenticate with that.
const CryptoJS = await altair.importModule('crypto-js');
const b64Decode = await altair.importModule('atob');

const collectEnvironmentVariables = () => {
  const clientId = altair.helpers.getEnvironment('AwsCognitoClientId')
  const clientSecret = altair.helpers.getEnvironment('AwsCognitoClientSecret')
  const region = altair.helpers.getEnvironment('AwsCognitoRegion')
  const username = altair.helpers.getEnvironment('AwsCognitoUsername')
  const password = altair.helpers.getEnvironment('AwsCognitoPassword')

  if ( !clientId || !clientSecret || !region || !username || !password) {
    altair.log("AWS Cognito set up is not complete. Missing ENV vars")
    throw new Error("AWS Cognito set up is not complete. Missing ENV vars")
  }

  altair.log("AWS Inputs: " + JSON.stringify({
    clientId,
    clientSecret: Boolean(clientSecret),
    region,
    username,
    password: Boolean(password)
  }))

  return {
    clientId,
    clientSecret,
    region,
    username,
    password
  }
}

const jwtPartLabels = new Map([
  [0, 'algo'],
  [1, 'content']
])

const decodeAWSCognitoJWTToken = jwtString => {
  return jwtString.split('.').reduce((parts, next, i) => {
    
    if (jwtPartLabels.has(i)) {
      parts.set(jwtPartLabels.get(i), JSON.parse(atob(next)))
    }

    return parts
  }, new Map())
}

const unixTimestampAtNow = () => {
  return Math.round(Date.now() / 1000)
}

const isAWSCognitoJWTokenValid  = jwtContent => {
  const { exp } = jwtContent
  return unixTimestampAtNow() < exp
}

const getRequestSecretHash = (requestSettings) => {
  return CryptoJS.enc.Base64.stringify(
    CryptoJS.HmacSHA256(
      [requestSettings.username, requestSettings.clientId].join(''),
      requestSettings.clientSecret
    )
  )
}

const getAWSCognitoAuthURI = (region) => {
  return `https://cognito-idp.${region}.amazonaws.com`
}

const generateRequestBody = requestSettings => {
  return {
    AuthFlow: "USER_PASSWORD_AUTH",
    ClientId: requestSettings.clientId,
    AuthParameters: {
      USERNAME: requestSettings.username,
      PASSWORD: requestSettings.password,
      SECRET_HASH: getRequestSecretHash(requestSettings)
    }
  }
}

const getNewAWSCognitoIdToken = async requestSettings => {
  const { AuthenticationResult: { IdToken } } = await altair.helpers.request(
    'POST', 
    getAWSCognitoAuthURI(requestSettings.region),
    {
      headers: {
        "content-type": "application/x-amz-json-1.1",
        "x-amz-target": "AWSCognitoIdentityProviderService.InitiateAuth",
      },
      body: JSON.stringify(generateRequestBody(requestSettings))
    }
  );

  altair.log("Id token retrieved: " + IdToken)
  return IdToken
}

const getExistingAWSCognitoSession = () => {
  return localStorage.getItem("AWSCognitoIdToken") || null
}

const getSessionAWSCognitoToken = () => {
  let token = getExistingAWSCognitoSession()
  
  if ( !token || ( token && !isAWSCognitoJWTokenValid(token) ) ) {
    // no existing session or it's expired, get a new one
    token = getNewAWSCognitoIdToken(collectEnvironmentVariables())
  } 

  return token 
}

const addAuthenticationToRequest = async () => {
  const token = await getSessionAWSCognitoToken()
  
  altair.helpers.setEnvironment('AwsCognitoIdToken', token)
}

await addAuthenticationToRequest()

@imolorhe
Copy link
Collaborator

@ortonomy Trying to split this up into multiple separate tasks.

I can't set headers from the pre-request script

The headers provided to the pre request scripts are read only, and any changes need to use the environment variable like you described. I think it's a valid point to be able to modify the headers directly.

the pre-request scripts are manual, and don't lend themselves to sharing

I'm not sure I understand if there's something to be fixed here, or if you're just stating a fact about pre-request scripts generally.

Re the plugin..

save these values to the selected environment

I am not sold on the idea of allowing the plugin interact with the environment variables just yet. In general, the environment variables tend to be the more sensitive part of the data.

before every request (including schema fetching) run a script ...

This seems to be the main part that is missing from the current plugin architecture that will enable you do what you want using a plugin. So basically, we need a new event triggered with a "blocking" callback (i.e. it is not just a regular event listener, but it should wait for the event listener to return before proceeding)

automatically set the Auth headers

We should also be able to set headers from plugins


Do these summarize the action items or did I miss anything?

@ortonomy
Copy link
Author

Hey @imolorhe - thanks for being so responsive and trying to address this. Sorry haven't responded. will make effort to edit this comment and respond today or tomorrow.

@ortonomy
Copy link
Author

ortonomy commented Feb 17, 2023

@imolorhe

Do these summarize the action items or did I miss anything?

Really really appreciate you feedback and analysis here. I agree with them all, especially:

So basically, we need a new event triggered with a "blocking" callback (i.e. it is not just a regular event listener, but it should wait for the event listener to return before proceeding)


Allow me to clarify your question

I said:

the pre-request scripts are manual, and don't lend themselves to sharing

You said:

I'm not sure I understand if there's something to be fixed here, or if you're just stating a fact about pre-request scripts generally.

It is stating a fact, but cannot act alone as a sentence withou context (which was split apart in my request)

I would like to make this a plugin I can share with others including my teammates, .... the pre-request scripts are manual, and don't lend themselves to sharing.

To emphasise: if I could write a plugin, I could offer a smooth way for teammates (existing or new) to get started with our AWS appsync grpahql without messy setting up of the pre-request script.


I am not sold on the idea of allowing the plugin interact with the environment variables just yet. In general, the environment variables tend to be the more sensitive part of the data.

I only suggested this because:

  • if I can't change headers in the script

  • assumed I may not be able to change headers in plugin

  • the only way to set headers right now is in left hand bar (not sure if per request or globally)

  • the only way to make that dynamic is via an ENV. so solution I was seeing (Which is the same solution that postman do - run a post-request script after auth request to extract token and set ENV var):

    • env with name X is set to blank
    • set env in headers
    • header is set to {{X}}

is that clear?

if you allow us to set headers in the plugin with a blocking request, yea, then the ENV setting it not needed.


as an aside one UX improvement I saw with altair which was somewhat confusing is that despite enabling the "collection" level pre-request script, it doesn't actually get enabled per request, unless you enable the pre-request script in the request. Which may be empty...

Suggest removing this limuitation. If you have a pre-request script at collection level and it's enabled, it should be executed regardless of if the request script itself has been enabled.

Thanks for working on a great app!

@imolorhe
Copy link
Collaborator

So for the environment variables, you only want to be able to set them and not read them?

@ortonomy
Copy link
Author

I think reading and setting them would give ultimately flexibility to plugin creators - you could chain plugins and pass data?

If you are making this update, please make the available plugin events available on docs pages. I had to dive into the code to find what was available.

@imolorhe
Copy link
Collaborator

I try to keep the docs (available docs) updated with relevant changes. I didn't add the events at all though 😅 so nothing to update (since I was concerned they will get stale quickly)

Anyway would appreciate any help on the documentation front.

@ortonomy
Copy link
Author

Sure @imolorhe - are docs for site in this repo? Add a PR from a fork?

Let me know the list of events and I'll update the plugin pages. I also couldn't find a full spec on the current versions plugin specification in the manifest.

I also want to update the description about a) using file:// as a URI for the local plugin testing rather than a URL - using a URL results in errors because of insecure resources

@imolorhe
Copy link
Collaborator

@ortonomy Yes! Docs are here.

Most of the context around plugins are here. Specifically the events and their payload types are here (these may change a bit since I will be adding a "blocking" listener, which we don't have at the moment).

All the actions from the plugin context are defined here.

@yuniers
Copy link

yuniers commented Apr 15, 2023

I need transform my altair.data.query with jose npm package, using "RSA-OAEP-256" algorithm and "A256GCM" encoder.
How I can do that from pre-request script?

@imolorhe
Copy link
Collaborator

imolorhe commented Apr 15, 2023

@yuniers I am not sure how you can do that easily. You'll need to pretty much copy and paste the entire code into the pre request script.

@yuniers
Copy link

yuniers commented Apr 16, 2023

@yuniers I am not sure how you can do that easily. You'll need to pretty much copy and paste the entire code into the pre request script.

I'm trying with next code.

await dynamicallyLoadScript('https://cdnjs.cloudflare.com/ajax/libs/jose/4.14.0/index.umd.min.js', 'module');
async function dynamicallyLoadScript(url, type = 'text/javascript') {
  try {
    let script = document.createElement('script');
    let content = await fetch(url);
    script.textContent = await content.text();
    script.type = type;
    document.head.appendChild(script);
  } catch (e) {
    console.log(e);
    throw e;
  }
}

But a CSP error is triggered. I can't do document.head.appendChild(script);

Content Security Policy: Las opciones para esta página han bloqueado la carga de un recurso en inline (script-src).

@imolorhe
Copy link
Collaborator

Yes you can't execute inline scripts on the page

@yuniers
Copy link

yuniers commented Apr 16, 2023

Yes you can't execute inline scripts on the page

Could be added another default module?

@imolorhe
Copy link
Collaborator

Yes that's the only other alternative

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants