182 lines
6.1 KiB
JavaScript
182 lines
6.1 KiB
JavaScript
// Copyright 2020 Red Hat, Inc
|
|
// Copyright 2021 Acme Gating, LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
import * as React from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import { connect } from 'react-redux'
|
|
|
|
import { AuthProvider } from 'oidc-react'
|
|
import { userLoggedIn, userLoggedOut } from './actions/user'
|
|
import { UserManager, User } from 'oidc-client'
|
|
import { getHomepageUrl } from './api'
|
|
import _ from 'lodash'
|
|
|
|
|
|
class ZuulAuthProvider extends React.Component {
|
|
/*
|
|
This wraps the oidc-react AuthProvider and supplies the necessary
|
|
information as props.
|
|
|
|
The oidc-react AuthProvider is not really meant to be reconstructed
|
|
frequently. Calling render multiple times (even if nothing actually
|
|
changes) during a login can cause multiple AuthProviders to be created
|
|
which can interfere with the login process.
|
|
|
|
We connect this class to state.auth.auth_params, so make sure that isn't
|
|
updated unless the OIDC parameters are actually changed.
|
|
|
|
If they are changed, then we will create a new AuthProvider with the
|
|
new parameters. Save those parameters in local storage so that when
|
|
we return from the IDP redirect, an AuthProvider with the same
|
|
configuration is created.
|
|
*/
|
|
static propTypes = {
|
|
auth_params: PropTypes.object,
|
|
channel: PropTypes.object,
|
|
election: PropTypes.object,
|
|
dispatch: PropTypes.func,
|
|
children: PropTypes.any,
|
|
}
|
|
|
|
render() {
|
|
const { auth_params, channel, election } = this.props
|
|
|
|
console.debug('ZuulAuthProvider rendering with params', auth_params)
|
|
|
|
const userManager = new UserManager({
|
|
...auth_params,
|
|
response_type: 'token id_token',
|
|
silent_redirect_uri: getHomepageUrl() + 'silent_callback',
|
|
redirect_uri: getHomepageUrl() + 'auth_callback',
|
|
includeIdTokenInSilentRenew: false,
|
|
})
|
|
|
|
const oidcConfig = {
|
|
onSignIn: async (user) => {
|
|
// Update redux with the logged in state and send the
|
|
// credentials to any other tabs.
|
|
this.props.dispatch(userLoggedIn(user))
|
|
this.props.channel.postMessage({
|
|
type: 'signIn',
|
|
auth_params: auth_params,
|
|
user: user
|
|
})
|
|
},
|
|
onSignOut: async () => {
|
|
// Update redux with the logged out state and send the
|
|
// credentials to any other tabs.
|
|
this.props.dispatch(userLoggedOut())
|
|
this.props.channel.postMessage({
|
|
type: 'signOut',
|
|
auth_params: auth_params
|
|
})
|
|
},
|
|
autoSignIn: false,
|
|
userManager: userManager,
|
|
}
|
|
|
|
// This is called whenever we receive a message from another tab
|
|
channel.onmessage = (msg) => {
|
|
console.debug('Received broadcast message', msg)
|
|
|
|
if (msg.type === 'signIn' && _.isEqual(msg.auth_params, auth_params)) {
|
|
const user = new User(msg.user)
|
|
userManager.getUser().then((olduser) => {
|
|
// In some cases, we can receive our own message, so make
|
|
// sure that the user info we received is different from
|
|
// what we already have.
|
|
let needToUpdate = true
|
|
if (olduser) {
|
|
if (user.toStorageString() === olduser.toStorageString()) {
|
|
needToUpdate = false
|
|
}
|
|
}
|
|
if (needToUpdate) {
|
|
console.debug('New token from other tab')
|
|
userManager.storeUser(user)
|
|
userManager.events.load(user)
|
|
this.props.dispatch(userLoggedIn(user))
|
|
}
|
|
})
|
|
}
|
|
else if (msg.type === 'signOut' && _.isEqual(msg.auth_params, auth_params)) {
|
|
userManager.removeUser()
|
|
this.props.dispatch(userLoggedOut())
|
|
}
|
|
else if (msg.type === 'init') {
|
|
// A new tab has been created; send our token in case it's helpful.
|
|
userManager.getUser().then((user) => {
|
|
if (user) {
|
|
console.debug('Sending token to new tab')
|
|
this.props.channel.postMessage({
|
|
type: 'signIn',
|
|
auth_params: auth_params,
|
|
user: user
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// If we already have user data saved in session storage, we need to
|
|
// tell redux about it.
|
|
userManager.getUser().then((user) => {
|
|
if (user) {
|
|
console.debug('Restoring initial login from userManager')
|
|
this.props.dispatch(userLoggedIn(user))
|
|
} else {
|
|
// Maybe another tab is logged in. Ask them to send us tokens.
|
|
console.debug('Asking other tabs for auth tokens')
|
|
this.props.channel.postMessage({ type: 'init' })
|
|
}
|
|
})
|
|
|
|
// This is called about 1 minute before a token is expired. We will try
|
|
// to renew the token. We use a leader election so that only one tab
|
|
// makes the attempt; the others will receive the token via a broadcast
|
|
// event.
|
|
userManager.events.addAccessTokenExpiring(() => {
|
|
if (election.isLeader) {
|
|
console.debug('Token is expiring; renewing')
|
|
userManager.signinSilent().then(user => {
|
|
console.debug('Token renewal successful')
|
|
this.props.dispatch(userLoggedIn(user))
|
|
channel.postMessage({
|
|
type: 'signIn',
|
|
auth_params: auth_params,
|
|
user: user
|
|
})
|
|
}, err => {
|
|
console.error('Error renewing token:', err.message)
|
|
})
|
|
} else {
|
|
console.debug('Token is expiring; expecting leader to renew')
|
|
}
|
|
})
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<AuthProvider {...oidcConfig} key={JSON.stringify(auth_params)}>
|
|
{this.props.children}
|
|
</AuthProvider>
|
|
</React.Fragment>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default connect(state => ({
|
|
auth_params: state.auth.auth_params,
|
|
}))(ZuulAuthProvider)
|