@pga/auth-provider
v0.0.35
Published
PGA React Auth Provider Package for OneLogin
Downloads
33
Readme
PGA React AuthProvider for OneLogin
This package provides React functionality for handling OneLogin authentication via our backend.
Important
We will be using the new unified OneLogin app for our apps with this new auth. Since the auth is handled on the backend, we don't need all the REACT_APP_ONELOGIN_*
env variables on the frontend anymore. However, we'll need to add REACT_APP_AUTH_BASE_URI=https://auth.sandboxpga.org
to our .env
files which is the unified auth base URI handling all auth-related callbacks and redirects (/login.html
, /callback.html
, /silent.html
etc.) ~~Once we get the auth on the sandbox
we should be able to use https://auth.sandboxpga.org
for local development, just like we've been using https://developers.sanboxpga.org/graphql
for the /graphql
endpoint while developing locally.~~
It also may be a good time to switch and sync all our React apps to using *.pga.local:DIFFERENT_PORTS_FOR_DIFFERENT_REACT_APPS
for local development so we can avoid the issues we've been having with using *.sandboxpga.org:SAME_PORT_FOR_DIFF_APPS
.
<AuthProvider />
The <AuthProvider />
component is intended to be used once per app. It's basically a React Context Provider (similar to <ApolloProvider />
, or React Router's <BrowserRouter />
, which wraps the authentication state and passes it to its consuming children. It makes sense to have it once at the top level React Component tree, within the ./src/index.js
(or ./src/App.js
) for example:
ReactDOM.render(
<ApolloProvider client={apolloClient}>
<BrowserRouter>
<AuthProvider apolloClient={apolloClient}>
<App />
</AuthProvider>
</BrowserRouter>
</ApolloProvider>,
document.getElementById('root')
)
We need to pass our apolloClient
as a prop because we use me
to query for users data and log in status.
Logged in users have the JWT token stored in auth.pga.org
's localStorage
(for .pga.org
). So what happens in the background, on AuthProvider
's componentDidMount()
method we start listening for messages from the auth relay iframe
and query /graphql
endpoint for me
to get user data, along with user's data if logged in:
componentDidMount () {
window.addEventListener('message', this.receiveAuthMessage)
}
async receiveAuthMessage (event) {
const { data, origin } = event
const { user, type } = data
const isValidType = origin === process.env.REACT_APP_AUTH_BASE_URI && type === 'pgaAuth'
if (isValidType) {
const { apolloClient } = this.props
const isLoggedIn = !!(user && user.id_token)
const authReady = true
if (!isLoggedIn) {
this.setState({ authReady, isLoggedIn, user })
clearToken()
} else {
setToken(user.id_token)
try {
const { data: { me }, errors } = await apolloClient.query({ query: GET_USER_DATA })
if (errors || !me) throw new Error(`User not logged in`)
this.setState({ authReady, isLoggedIn, user, me })
} catch (err) {
this.setState({ authReady, isLoggedIn: false, user: null, me: null })
clearToken()
}
}
}
}
Important to note that we do not wait for the query to resolve and then render the children
(as we did with our previous frontend-based auth), but we render them instantly:
render () {
const authRelayUrl = `${process.env.REACT_APP_AUTH_BASE_URI}/relay.html?parent=${window.location.origin}`
return (
<Provider value={this.state}>
{this.props.children}
<AuthRelayFrame src={authRelayUrl} />
</Provider>
)
}
However, we pass on the authReady
property along with the provided auth state to be consumed by the children
. This way we render the public
routes asap regardless of the login status (as we should), and make it <PrivateRoute />
's responsibility to restrict the access to authenticated users only, and wait for authReady
if needed.
withAuthProvider
A HOC
wrapper around our AuthConsumer
(a React Context Consumer) which passes the auth provider state
to the wrapped component. This is helpful when we need to pass the auth state to our components. For example in our <AuthHeader />
, where we need to know whether the user is logged in and access user's data (avatar, email, name etc.) we may use withAuthProvider(AuthHeader)
. Right now the data we are passing to the wrapped components looks like this:
{
authReady: true, // (true | false) — whether we've loaded the initial session from the server
isLoggedIn: false, // (true | false) — user's login status
user: {/* ... */} // (Object | null) - OneLogin's token data (id_token, profile, custom_fields ...)
me: { // (Object | null) — user's data
class: 'B6',
firstName: 'John',
id: '12345678',
lastName: 'Doe',
phoneNumber: '5555555555',
photo: null,
primaryEmail: '[email protected]',
type: 'MB',
__typename: 'Member'
}
}
<PrivateRoute />
<PrivateRoute />
requires authentication, or it redirects the user for login. It's basically a Route Component wrapped in a React Context Consumer (our AuthConsumer) with AuthProvider
's withAuthProvider()
. It gets AuthProvider
's state ({ isLoggedIn, authReady, user, me }
), and additionally we can pass in returnTo
prop (if accessing the route without being authenticated) — the URL to return the users to once they log in at OneLogin — defaults to the route trying to access.