-
Notifications
You must be signed in to change notification settings - Fork 70
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
PCF-400 Query https://firefox-ci-tc.services.mozilla.com/login/oauth/credentials to verify token and request credentials #648
PCF-400 Query https://firefox-ci-tc.services.mozilla.com/login/oauth/credentials to verify token and request credentials #648
Conversation
✅ Deploy Preview for mozilla-perfcompare ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## beta #648 +/- ##
==========================================
- Coverage 91.80% 91.41% -0.39%
==========================================
Files 68 69 +1
Lines 1587 1620 +33
Branches 285 291 +6
==========================================
+ Hits 1457 1481 +24
- Misses 104 111 +7
- Partials 26 28 +2 ☔ View full report in Codecov by Sentry. |
ebef7d2
to
5fd086b
Compare
5fd086b
to
e3642f7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey andra, thanks for this work!
here are a few comments, I hope they make sense.
In addition to the comments on the code itself, there's another missing bit IMO.
Here is the current workflow:
- The user is in tab A and clicks on the "login" button in the modal
- open a new tab B to taskcluster
- in tab B the user logs in on task cluster
- in tab B taskcluster redirects on our callback
- in tab B perfcompare requests and retrieves the access token to the taskcluster server
- in tab B perfcompare requests and retrieves the user credentials to the taskcluster server
- in tab B perfcompare stores the token to localStorage
- ... the missing link
For step 8, we can add an event listener on the "storage" event in tab A so that we're notified with the user credentials change. We can add it in step 1.
(see MDN: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event)
I'm thinking that if step 5 stores the access token to the localStorage, then step 6-7 could be done in tab A... it might be a better design actually, because (as I write below in another comment) in the future I'd like that we don't need to open a tab if we already have an unexpired access token.
So here is how the workflow could look like (bold for changes):
- The user is in tab A and clicks on the "login" button in the modal.
- adds an event listener for "storage", and opens a new tab B to taskcluster
- in tab B the user logs in on task cluster
- in tab B taskcluster redirects on our callback
- in tab B perfcompare requests and retrieves the access token to the taskcluster server
- in tab B perfcompare stores the access token to localStorage, then closes the tab with
window.close
- in tab A, perfcompare is notified from the "storage" event.
- in tab A perfcompare requests and retrieves the user credentials to the taskcluster server
- in tab A perfcompare stores the token to localStorage
- in tab A perfcompare does the retrigger request
I don't request to do this workflow change in this PR, but it could be good to do it in a next PR, if you don't mind.
Tell me what you think (and if I'm missing anything)!
src/logic/taskcluster.ts
Outdated
}; | ||
|
||
// fetch Taskcluster credentials using token Bearer | ||
const response = await fetchData( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetchData returns Promise but my understanding is that the object returned here is different, with different properties, so you need a different type than ResponseToken
.
See above my suggestion about how to change fetchData
, that should make it easier to control the return value.
The type for the user credentials will be necessary as well when we'll get the data from the localStorage (cf my other comment about creating a separate file).
|
||
return tokenBearer; | ||
sessionStorage.setItem('userCredentials', JSON.stringify(accessToken)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this one should be in localStorage because we want to keep it for a longer time, not just for the duration of the browser session.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking it could be good to encapsulate this logic in a separate file, so that we can type inputs and outputs.
This could be a file src/logic/credentials-storage.ts
(for example) with functions such as:
function storeUserCredentials(UserCredentials credentials) {
localStorage.setItem('userCredentials', JSON.stringify(credentials));
}
function retrieveUserCredentials(): UserCredentials {
return JSON.parse(localStorage.getItem('userCredentials'));
}
// same for storeTaskclusterToken and retrieveTaskclusterToken
I'm thinking these functions could be in the existing file logic/taskcluster.ts
too, as you wish.
src/logic/taskcluster.ts
Outdated
interface RequestOptions { | ||
method: string; | ||
body: URLSearchParams; | ||
method?: string; | ||
body?: URLSearchParams; | ||
headers: { | ||
Authorization?: string; | ||
'Content-Type': string; | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having fetchData
doing the request, my suggestion would be to have a function to check error cases:
function checkTaskclusterResponse(response: Response) {
if (!response.ok) {
if (response.status === 400) {
throw new Error(
`Error when requesting Taskcluster: ${await response.text()}`,
);
} else {
throw new Error(
`Error when requesting Taskcluster: (${response.status}) ${response.statusText}`,
);
}
}}
Then you can move the rest of the function (fetch call + call to json()) back to retrieveTaskclusterToken
and retrieveTaskclusterAccessToken
, and you don't need to have this RequestOptions type anymore.
src/logic/taskcluster.ts
Outdated
@@ -133,3 +134,23 @@ export async function retrieveTaskclusterToken(rootUrl: string, code: string) { | |||
|
|||
return response; | |||
} | |||
|
|||
export async function retrieveTaskclusterAccessToken( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: please call this retrieveTaskclusterUserCredentials
so that it's better distinguished with the other function.
}); | ||
mockedGetLocationOrigin.mockImplementation(() => 'http://localhost:3000'); | ||
|
||
expect(window.fetch).toHaveBeenLastCalledWith( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test problem is that we're not waiting enough in this test. Indeed the test is synchronous but the code is asynchronous, so the second fetch
call hasn't happened yet!
The fix is to wait for the rendering.
For example you can add:
await screen.findByText(/Getting Taskcluster credentials/);
As you see the component will be displayed only after the loader is completely rendered. If you want it to render before and change the text, you could use this guide: https://reactrouter.com/en/main/guides/deferred
But I think this is for a later PR, let's make it work end to end first!
src/types/types.ts
Outdated
string, | ||
{ expires: string; credentials: { clientId: string; accessToken: string } } | ||
>; | ||
|
||
export type TokenBearer = Record< | ||
string, | ||
{ access_token: string; token_type: 'Bearer' } | ||
>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these 2 types are incorrect, they're not Record<string, XXX>
, but directly XXX
.
export type UserCredentials = { expires: string; credentials: { clientId: string; accessToken: string } };
export type TokenBearer = { access_token: string; token_type: 'Bearer' };
A Record
is like a Map (but using an object), also called a dictionary. This isn't what it is here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed that the Treeherder implementation stores the userCredentials in this way using the url.
userCredentials "https://firefox-ci-tc.services.mozilla.com": {
expires: ...
credentials: {
clientId: ...
accessToken: ...
}
}
I believe that's why the Record for userCredentials. Should we still go with removing the Record for userCredentials?
2a43d43
to
9ad5597
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
here are a few more comments to fix before landing, thanks again for your work Andra
84707cf
to
d12b2e2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this with fresh eyes, here are some last minute changes, sorry about that.
Also have you seen my longer comment in #648 (review) ? Any comment about that? Thanks
taskclusterCode, | ||
); | ||
|
||
storeToken({ [tokenResponse.token_type]: tokenResponse }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should look like this:
storeToken({ [tokenResponse.token_type]: tokenResponse }); | |
storeToken({ [rootUrl]: tokenResponse }); |
but please look at my other comments as well, as I believe the signature will be changed and therefore this will look like this:
storeToken({ [tokenResponse.token_type]: tokenResponse }); | |
storeToken(rootUrl, tokenResponse); |
src/logic/credentials-storage.ts
Outdated
export function storeUserCredentials(credentials: UserCredentials) { | ||
localStorage.setItem('userCredentials', JSON.stringify(credentials)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal for storing a Record is that we can add separate entries for different URLs. Therefore I believe the implementation should probably look lile this:
export function storeUserCredentials(credentials: UserCredentials) { | |
localStorage.setItem('userCredentials', JSON.stringify(credentials)); | |
} | |
export function storeUserCredentials(rootUrl: string, credentials: UserCredentialsResponse) { | |
const allCredentialsAsString = localStorage.userCredentials; | |
const allCredentials = (allCredentialsAsString ? JSON.parse(allCredentialsAsString) : {}) as UserCredentials; | |
allCredentials[rootUrl] = credentials; | |
localStorage.userCredentials = JSON.stringify(allCredentials); | |
} |
Similar for for storeToken.
src/logic/credentials-storage.ts
Outdated
export function retrieveUserCredentials(): UserCredentials { | ||
return JSON.parse( | ||
localStorage.getItem('userCredentials') as string, | ||
) as UserCredentials; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise, the retrieve functions could look lile this:
export function retrieveUserCredentials(): UserCredentials { | |
return JSON.parse( | |
localStorage.getItem('userCredentials') as string, | |
) as UserCredentials; | |
} | |
export function retrieveUserCredentials(rootUrl: string): UserCredentialsResponse | null { | |
const allCredentialsAsString = localStorage.userCredentials; | |
if (!allCredentialsAsString) { | |
return null; | |
} | |
const allCredentials = JSON.parse(allCredentialsAsString) as UserCredentials; | |
return allCredentials[rootUrl] ?? null; | |
} |
and similarly for the other retrieve function.
850c7ff
to
cb68695
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's land this and move forward!
src/logic/credentials-storage.ts
Outdated
if (allCredentialsAsString) return null; | ||
|
||
const allCredentials = JSON.parse(allCredentialsAsString) as UserCredentials; | ||
return allCredentials[rootUrl]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit
return allCredentials[rootUrl]; | |
return allCredentials[rootUrl] ?? null; |
(same as above)
src/logic/credentials-storage.ts
Outdated
if (allUserTokensAsString) return null; | ||
|
||
const allTokens = JSON.parse(allUserTokensAsString) as UserToken; | ||
return allTokens[rootUrl]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit
return allTokens[rootUrl]; | |
return allTokens[rootUrl] ?? null; |
otherwise that can return undefined -- typescript doesn't check this by default.
Not a super big deal but let's not lie to typescript.
src/types/types.ts
Outdated
export type TokenBearerResponse = { | ||
access_token: string; | ||
token_type: 'Bearer'; | ||
}; | ||
|
||
export type TokenBearer = Record<string, TokenBearerResponse>; | ||
|
||
export type TokenResponse = TokenBearerResponse; | ||
|
||
export type UserToken = Record<string, TokenResponse>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it looks like you have the same types twice:
TokenBearerResponse == TokenResponse
TokenBearer == UserToken
Please pick just one pair (I'd suggest TokenBearerResponse / TokenBearer)
Optional nit: it would be good to move the directionary types to credentials-storage.ts
because they're used only there, they're not global.
Optional nit: Rename:
CredentialsResponse => UserCredentials
UserCredentials => UserCredentialsDictionary
TokenBearerResponse => TokenBearer
TokenBearer => TokenBearerDictionary
(with the Dictionary types in credentials-storage.ts as suggested above, and the other types kept here)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! I was confused about how to keep them.
0775dd8
to
09bdd23
Compare
09bdd23
to
f69924a
Compare
This PR fetches the Taskcluster credentials (with an access token that has expiration date) using access token Bearer.