Open ortonomy opened 1 year ago
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?
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:
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:
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()
@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?
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.
@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:
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):
X
is set to blank{{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!
So for the environment variables, you only want to be able to set them and not read them?
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.
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.
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
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?
@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 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).
Yes you can't execute inline scripts on the page
Yes you can't execute inline scripts on the page
Could be added another default module?
Yes that's the only other alternative
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
libI wanted to write my plugin such that it could satisfy:
we need:
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