-
Notifications
You must be signed in to change notification settings - Fork 4
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
[DUOS-2462][DUOS-2850] B2C OAuth Integration #2512
Conversation
fd2c720
to
1ca9f86
Compare
85ba8f9
to
407faf7
Compare
…gnInButton errors
const metadata: Partial<OidcMetadata> = { | ||
authorization_endpoint: `${await Config.getApiUrl()}/oauth2/authorize`, | ||
token_endpoint: `${await Config.getApiUrl()}/oauth2/token`, | ||
}; |
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 don't know the right answer here, but I wonder if adding this metadata is required. Does oidc-client-ts
know how to use the authority config value to lookup the .well-known/openid-configuration
? If so, we probably don't need to use the custom authorization and token endpoints.
Note that the custom endpoints were ported by me from TDR. I didn't fully understand why it has many of the custom bits. I now realize that it was mostly to be able to support switching back and forth between B2C and Google. All of that is over with and we are B2C all the way. So the custom endpoints don't really provide any value. They are used by the swagger ui but I have figured out a better way that does not require them.
Bottom line: I think we should work towards removing these endpoints.
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.
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.
looks like our config url (in dev) does not have CORS headers set. We could look into setting those or add a new DUOS api to serve the config. It actually already gets and caches the config, there is jut no api.
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 all for removing the oauth2 proxy endpoints if they are not needed anymore. I think another thing the custom endpoints do is inject the clientId as a scope and add ?prompt=login
to the authorize
request. See:
- https://github.com/broadinstitute/terra-helmfile/blob/fa1dabdd92833cb4e736e7203d7ef6d0e315b3a3/charts/consent/templates/_consent.yaml.tpl#L69-L73
- https://github.com/DataBiosphere/consent/blob/develop/src/main/java/org/broadinstitute/consent/http/service/OidcService.java#L33-L36
There is probably a way to set these in the oidc-client-ts
frontend instead.
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.
Extra parameters can be set with the extraQueryParams config setting.
prompt=login
is good but I don't know if the client id is required. In my experimentation with the swagger UI's oidc integration it doesn't seem to be.
silent_redirect_uri: `${window.origin}/redirect-from-oauth-silent`, | ||
metadata, | ||
prompt: 'consent login', | ||
scope: 'openid email profile', |
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.
Re my above comment: I see prompt=login
is already set here. We'd need to figure out how to set the clientId as a scope if not using the custom oauth2 endpoints. (Or validate that this is still a requirement in B2C?)
src/libs/auth/oidcBroker.ts
Outdated
}, | ||
|
||
signOut: async (): Promise<void> => { | ||
await OidcBroker.getUserManager().removeUser(); |
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.
Just wondering if we have to clear any storage upon signout, or does removeUser
take care of it?
(Just remembering that terra-ui had to clear several things upon signOut, but that might have been Terra UI's custom state store.)
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 is probably worth throwing in a clearStaleState
(https://authts.github.io/oidc-client-ts/classes/UserManager.html#clearStaleState) here just to clean out anything else that may be in the store. The specific duos-ui
state is cleared out in Auth.signOut
. Good callout 👍
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.
Overall it looks good to me!
I do think it's an interesting exploration whether the backend oauth2 proxy endpoints are truly needed anymore..
return <div>{!showError ? signInButton() : errorAlert(errorInfo)}</div>; | ||
}; | ||
|
||
export default SignInButton; |
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.
Just for future pr's it is nice for these sorts of js -> ts conversions to have inline pr comments on the new file for reviewers of what you changed relative to the js file since there is no direct diff shown in github. I just opened up both side by side and did the comparison for this pr.
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.
Yeah, I definitely agree it is hard to tell the difference easily. In hindsight, I should have done this TS conversion in a separate PR and then made the b2c changes in this one.
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 looks good overall, I would definitely smoke test this thoroughly and expect there to maybe be some unforeseen behavior/bugs given the changes. In particular make sure that the background token expiration/refresh works as expected etc. I think you set yourself up well with logging and metrics collection.
src/Routes.jsx
Outdated
<Route exact path="/backgroundsignin" render={ | ||
(routeProps) => | ||
checkEnv(envGroups.NON_STAGING) | ||
? <BackgroundSignIn {...routeProps} /> | ||
: <NotFound /> | ||
} /> |
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 page is what we use for E2E tests so we didn't have to manually trigger an auth dance. Currently, they're not run on PRs or in GHA, but we can run it locally. We either need a B2C version of this, or bring it back so we can test authenticated pages.
// TODO: Determine if this is the correct user flow | ||
// Can the user only accept ToS once the have signed in? | ||
// What state is the user in after signIn, but before accepting ToS? |
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 is the correct flow. Yes, the user can only accept ToS after signing in - otherwise we don't know who they are and could not register their ToS acceptance in Sam. If they sign in, but have not yet accepted ToS, they are directed to this page where their only choice is to accept or reject.
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.
That makes sense. I was trying to determine if the ToS accept button should set the setUserIsLogged
to true, since by signing in, userIsLogged should already be set to true. With this context, I believe the ToS accept button should not change the userIsLogged state.
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.
Correct - userIsLogged
state is separate and distinct from ToS acceptance.
@@ -2,7 +2,7 @@ | |||
|
|||
import React from 'react'; | |||
import { mount } from 'cypress/react'; | |||
import SignIn from '../../../src/components/SignIn'; | |||
import SignInButton from '../../../src/components/SignInButton'; | |||
import { Config } from '../../../src/libs/config'; | |||
|
|||
const signInText = 'Sign-in'; |
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 text is no longer accurate - the first test should pass with
const signInText = 'Sign-in'; | |
const signInText = 'Sign In'; |
it('Spinner loads when client id is empty', function () { | ||
cy.viewport(600, 300); | ||
cy.stub(Config, 'getGoogleClientId').returns(''); | ||
mount(<SignIn />); | ||
mount(<SignInButton />); | ||
cy.contains(signInText).should('not.exist'); | ||
}); |
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 don't think we need this test any longer - there is longer a spinner that loads in place of the sign in text during the google client id loading time.
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'll update this test with the new SignInButton
functionality.
.eslintrc
Outdated
// `any` is useful for incremental type improvements during the transition from JS to TS. | ||
"@typescript-eslint/no-explicit-any": "off", |
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.
Depending on how often an explicit any
is desired, consider disabling this for specific lines or files instead of in global ESLint config. That way, it increases the friction to adding any
and makes it easier to ratchet up type safety while incrementally removing any
later.
src/components/DuosHeader.jsx
Outdated
<div | ||
style={{ display: 'flex', alignItems: 'center', flexDirection: orientation === 'vertical' ? 'column' : 'row' }} | ||
> | ||
<SignInButton customStyle={{ whiteSpace: 'nowrap', }}></SignInButton> |
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:
<SignInButton customStyle={{ whiteSpace: 'nowrap', }}></SignInButton> | |
<SignInButton customStyle={{ whiteSpace: 'nowrap', }} /> |
src/components/SignInButton.tsx
Outdated
const { tosAccepted } = userStatus; | ||
if (!isEmpty(userStatus) && !tosAccepted) { |
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.
If userStatus
was empty, wouldn't the destructuring in const { tosAccepted } = userStatus;
throw an error?
When would we expect userStatus
to be empty?
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.
When would we expect
userStatus
to be empty?
I believe userStatus
is one of the flavors of the Sam user info object. We collect that on every back end API call as part of the DUOS user to determine ToS acceptance.
export const OAuth2 = { | ||
getConfig: async (): Promise<OAuthConfig> => getConfig(), | ||
}; |
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.
For what it's worth, as an alternative to namespace objects, modules could export individual methods and consumers could:
import * as OAuth2 from 'path/to/OAuth2';
if they really want the namespace.
src/libs/auth/RedirectFromOAuth.ts
Outdated
const userManager: UserManager = new UserManager( | ||
OidcBroker.getUserManagerSettings() | ||
); | ||
const url = window.location.href; | ||
const isSilent = window.location.pathname.startsWith( | ||
'/redirect-from-oauth-silent' | ||
); | ||
|
||
if (isSilent) { | ||
userManager.signinSilentCallback(url); | ||
} else { | ||
userManager.signinPopupCallback(url); | ||
} |
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 looks like it belongs in a useEffect
hook instead of in render
.
But does this need to be in a React component at all? It looks like oauth-redirect-loader
could call the UserManager
functions directly (and render a Spinner if desired).
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 don't think this needs to be in a React component since this should only happen once each time the user signs in through the popup window, and then I believe that app instance ends once that window closes.
src/libs/auth/RedirectFromOAuth.ts
Outdated
} else { | ||
userManager.signinPopupCallback(url); | ||
} | ||
return Spinner; |
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.
return Spinner; | |
return <Spinner />; |
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.
Seems like spinner was not a proper React component. Since it was only used in one other place, I converted it to one in order to make this change.
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.
Oh. I assumed it was based on the name. But if it was an element instead, that change will likely break other places it was used.
src/libs/auth/auth.ts
Outdated
signIn: async ( | ||
popup: boolean, | ||
onSuccess?: (response: OidcUser) => Promise<void> | void, | ||
onFailure?: (response: any) => Promise<void> | void | ||
): Promise<void> => { |
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.
Since this is already returning a Promise, consider leaning into that pattern instead of mixing it with callbacks.
Consumers could handle success or failure with signIn(...).then(onSuccess, onFailure)
.
let config: OAuthConfig | null = null; | ||
let userManagerSettings: UserManagerSettings | null = null; | ||
let userManager: UserManager | null = null; |
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.
A possible alternative to global state could be to have an OidcBroker class and have this module export a singleton instance of it?
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.
See comments inline - we need at least one test fix and the background sign-in feature replaced. Outside of that, this works as expected. I've been able to test standard OAuth with google users and OAuth with an azure account.
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 was able to run the PR after correctly making certain dependencies explicit. After limited testing, I was able to log in and out of my Google and Azure accounts and verify that the sign in button was in the login bar.
# Conflicts: # package-lock.json # package.json
# Conflicts: # .eslintrc # src/components/Spinner.tsx # src/custom.d.ts # tsconfig.json
#2611 Will continue this work as this PR too large to easily track the changes. |
Addresses
https://broadworkbench.atlassian.net/browse/DUOS-2462
https://broadworkbench.atlassian.net/browse/DUOS-2850
Summary
B2C
OidcBroker.ts
for wrapping calls to theoidc-client-ts
libraryAuth.ts
for managing DUOS UI specific session logic and communicating to the OidcBrokerOAuth2.ts
for making api calls to get our oauth configappLoader.ts
,RedirectFromOAuth.ts
to handle redirect after signing in through popup windowGoogleIS
and related codeOther
Home.jsx
toDuosHeader.jsx
Auth.signOut