Skip to content

Commit

Permalink
fix: Upgrades type/node and modifies monkey patching of fetch #190
Browse files Browse the repository at this point in the history
* Upgrades type/node to 18.18.9

* The upgrade of type/node causes an incompatibility with the overriding
  of fetch where the API is verbose and difficult to patch. As we are
  simply wanting an interceptor to supply the authorization header, we can
  employ fetch-intercept instead. This registers a function that jumps in
  front of the request before it is sent, allowing the auth header to be
  added.

* Supercedes PR #190
  • Loading branch information
phantomjinx committed Nov 17, 2023
1 parent 623e27b commit 4db3fd5
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 110 deletions.
2 changes: 1 addition & 1 deletion packages/kubernetes-api-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@patternfly/react-styles": "^4.92.6",
"@patternfly/react-table": "^4.113.0",
"@patternfly/react-tokens": "^4.94.6",
"@types/node": "^18.17.18",
"@types/node": "^18.18.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"mini-css-extract-plugin": "2.7.6",
Expand Down
2 changes: 1 addition & 1 deletion packages/kubernetes-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@hawtio/react": "^0.6.1",
"@types/jquery": "^3.5.25",
"@types/jsonpath": "^0.2.0",
"@types/node": "^18.17.18",
"@types/node": "^18.18.9",
"eventemitter3": "^5.0.1",
"jquery": "^3.7.0",
"js-logger": "^1.6.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/management-api-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@patternfly/react-styles": "^4.92.6",
"@patternfly/react-table": "^4.113.0",
"@patternfly/react-tokens": "^4.94.6",
"@types/node": "^18.17.18",
"@types/node": "^18.18.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"mini-css-extract-plugin": "2.7.6",
Expand Down
25 changes: 22 additions & 3 deletions packages/management-api-app/src/management.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const Management: React.FunctionComponent = () => {
const [error, setError] = useState<Error | null>()
const [username, setUsername] = useState('')

const [pods, setPods] = useState<ManagedPod[]>([])
const [pods, setPods] = useState<ManagedPod[] | null>(null)

useEffect(() => {
setIsLoading(true)
Expand All @@ -49,6 +49,10 @@ export const Management: React.FunctionComponent = () => {
return
}

// Make pods empty rather than null to
// show management loaded
setPods([])

await userService.fetchUser()
const username = await userService.getUsername()
setUsername(username)
Expand Down Expand Up @@ -123,7 +127,7 @@ export const Management: React.FunctionComponent = () => {
</MastheadContent>
</Masthead>

{pods.length === 0 && (
{pods === null && (
<Panel>
<Divider />

Expand All @@ -141,7 +145,22 @@ export const Management: React.FunctionComponent = () => {
</Panel>
)}

{pods.length > 0 && (
{pods !== null && pods.length === 0 && (
<Panel>
<Divider />
<Bullseye>
<div style={{ justifyContent: 'center' }}>
<TextContent>
<Text className={'--pf-global--Color--200'} component={TextVariants.h3}>
No Pods available
</Text>
</TextContent>
</div>
</Bullseye>
</Panel>
)}

{pods !== null && pods.length > 0 && (
<Panel>
<PanelHeader>API Properties</PanelHeader>
<Divider />
Expand Down
2 changes: 1 addition & 1 deletion packages/oauth-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@patternfly/react-styles": "^4.92.6",
"@patternfly/react-table": "^4.113.0",
"@patternfly/react-tokens": "^4.94.6",
"@types/node": "^18.17.18",
"@types/node": "^18.18.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"mini-css-extract-plugin": "2.7.6",
Expand Down
4 changes: 3 additions & 1 deletion packages/oauth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@
"dependencies": {
"@hawtio/react": "^0.6.1",
"babel-jest": "^29.6.1",
"fetch-intercept": "^2.4.0",
"jest-extended": "^4.0.0",
"jquery": "^3.7.0",
"js-logger": "^1.6.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"whatwg-fetch": "^3.6.19"
},
"devDependencies": {
"@types/jest": "^29.5.8",
Expand Down
47 changes: 28 additions & 19 deletions packages/oauth/src/form/form-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import $ from 'jquery'
import * as fetchIntercept from 'fetch-intercept'
import { getCookie } from '../utils'
import { log, OAuthProtoService, UserProfile } from '../globals'
import { FormConfig, FORM_TOKEN_STORAGE_KEY, FORM_AUTH_PROTOCOL_MODULE, ResolveUser } from './globals'
Expand All @@ -10,17 +11,24 @@ type LoginOptions = {
uri: URL
}

interface Headers {
Authorization: string
'X-XSRF-TOKEN'?: string
}

export class FormService implements OAuthProtoService {
private userProfile: UserProfile
private formConfig: FormConfig | null
private loggedIn: boolean
private fetchUnregister: (() => void) | null

constructor(formConfig: FormConfig | null, userProfile: UserProfile) {
log.debug('Initialising Form Auth Service')
this.userProfile = userProfile
this.userProfile.setOAuthType(FORM_AUTH_PROTOCOL_MODULE)
this.formConfig = formConfig
this.loggedIn = this.initLogin()
this.fetchUnregister = null
}

private initLogin(): boolean {
Expand Down Expand Up @@ -111,28 +119,27 @@ export class FormService implements OAuthProtoService {
}

log.debug('Intercept Fetch API to attach auth token to authorization header')
const { fetch: originalFetch } = window
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
log.debug('Fetch intercepted for oAuth authentication')

init = { ...init }
init.headers = {
...init.headers,
Authorization: `Bearer ${this.userProfile.getToken()}`,
}
this.fetchUnregister = fetchIntercept.register({
request: (url, config) => {
log.debug('Fetch intercepted for oAuth authentication')

// For CSRF protection with Spring Security
const token = getCookie('XSRF-TOKEN')
if (token) {
log.debug('Set XSRF token header from cookies')
init.headers = {
...init.headers,
'X-XSRF-TOKEN': token,
let headers: Headers = {
Authorization: `Bearer ${this.userProfile.getToken()}`,
}
}

return originalFetch(input, init)
}
// For CSRF protection with Spring Security
const token = getCookie('XSRF-TOKEN')
if (token) {
log.debug('Set XSRF token header from cookies')
headers = {
...headers,
'X-XSRF-TOKEN': token,
}
}

return [url, { headers, ...config }]
},
})
}

private setupJQueryAjax() {
Expand Down Expand Up @@ -163,6 +170,8 @@ export class FormService implements OAuthProtoService {
}

doLogout(): void {
if (this.fetchUnregister) this.fetchUnregister()

const currentURI = new URL(window.location.href)
this.forceRelogin(currentURI)
}
Expand Down
83 changes: 52 additions & 31 deletions packages/oauth/src/openshift/osoauth-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import $ from 'jquery'
import * as fetchIntercept from 'fetch-intercept'
import { log, OAuthProtoService, UserProfile } from '../globals'
import { fetchPath, isBlank, getCookie } from '../utils'
import { CLUSTER_CONSOLE_KEY } from '../metadata'
Expand All @@ -12,7 +13,7 @@ import {
OpenShiftOAuthConfig,
ResolveUser,
} from './globals'
import { buildUserInfoUri, checkToken, currentTimeSeconds, doLogout, tokenHasExpired } from './support'
import { buildUserInfoUri, checkToken, currentTimeSeconds, forceRelogin, tokenHasExpired } from './support'
import { userService } from '@hawtio/react'

interface UserObject {
Expand All @@ -26,6 +27,11 @@ interface UserObject {
groups: string[]
}

interface Headers {
Authorization: string
'X-XSRF-TOKEN'?: string
}

export class OSOAuthService implements OAuthProtoService {
private userInfoUri = ''
private keepaliveInterval = 10
Expand All @@ -34,13 +40,15 @@ export class OSOAuthService implements OAuthProtoService {
private userProfile: UserProfile
private readonly adaptedConfig: Promise<OpenShiftOAuthConfig | null>
private readonly login: Promise<boolean>
private fetchUnregister: (() => void) | null

constructor(openShiftConfig: OpenShiftOAuthConfig, userProfile: UserProfile) {
log.debug('Initialising Openshift OAuth Service')
this.userProfile = userProfile
this.userProfile.setOAuthType(OAUTH_OS_PROTOCOL_MODULE)
this.adaptedConfig = this.processConfig(openShiftConfig)
this.login = this.createLogin()
this.fetchUnregister = null
}

private async processConfig(config: OpenShiftOAuthConfig): Promise<OpenShiftOAuthConfig | null> {
Expand Down Expand Up @@ -95,37 +103,37 @@ export class OSOAuthService implements OAuthProtoService {
}

log.debug('Intercept Fetch API to attach Openshift auth token to authorization header')
const { fetch: originalFetch } = window
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
log.debug('Fetch intercepted for oAuth authentication')
const unregister = fetchIntercept.register({
request: (url, config) => {
log.debug('Fetch intercepted for oAuth authentication')

if (tokenHasExpired(this.userProfile)) {
return new Promise((resolve, _) => {
const reason = `Cannot navigate to ${input} as token expired so need to logout`
if (tokenHasExpired(this.userProfile)) {
const reason = `Cannot navigate to ${url} as token expired so need to logout`
log.debug(reason)
doLogout(config)
resolve(Response.error())
})
}

init = { ...init }
init.headers = {
...init.headers,
Authorization: `Bearer ${this.userProfile.getToken()}`,
}
// Unregister this fetch handler before logging out
unregister()

// For CSRF protection with Spring Security
const token = getCookie('XSRF-TOKEN')
if (token) {
log.debug('Set XSRF token header from cookies')
init.headers = {
...init.headers,
'X-XSRF-TOKEN': token,
this.doLogout(config)
}
}

return originalFetch(input, init)
}
let headers: Headers = {
Authorization: `Bearer ${this.userProfile.getToken()}`,
}

// For CSRF protection with Spring Security
const token = getCookie('XSRF-TOKEN')
if (token) {
log.debug('Set XSRF token header from cookies')
headers = {
...headers,
'X-XSRF-TOKEN': token,
}
}

return [url, { headers, ...config }]
},
})
}

private setupJQueryAjax(config: OpenShiftOAuthConfig) {
Expand All @@ -137,7 +145,7 @@ export class OSOAuthService implements OAuthProtoService {
const beforeSend = (xhr: JQueryXHR, settings: JQueryAjaxSettings) => {
if (tokenHasExpired(this.userProfile)) {
log.debug(`Cannot navigate to ${settings.url} as token expired so need to logout`)
doLogout(config)
this.doLogout(config)
return
}

Expand Down Expand Up @@ -187,7 +195,7 @@ export class OSOAuthService implements OAuthProtoService {
// In that case, let's just skip the error and go through another refresh cycle.
// See http://stackoverflow.com/questions/2000609/jquery-ajax-status-code-0 for more details.
log.error('Failed to fetch user info, status: ', response.statusText)
doLogout(config)
this.doLogout(config)
}
}

Expand All @@ -206,7 +214,7 @@ export class OSOAuthService implements OAuthProtoService {

if (tokenHasExpired(this.userProfile)) {
log.debug('Token has expired so logging out')
doLogout(config)
this.doLogout(config)
return true
}

Expand All @@ -233,7 +241,7 @@ export class OSOAuthService implements OAuthProtoService {
const tokenParams = checkToken(currentURI)
if (!tokenParams) {
log.debug('No Token so initiating new login')
doLogout(config)
this.doLogout(config)
return false
}

Expand Down Expand Up @@ -263,6 +271,19 @@ export class OSOAuthService implements OAuthProtoService {
}
}

private doLogout(config: OpenShiftOAuthConfig): void {
if (this.fetchUnregister) this.fetchUnregister()

const currentURI = new URL(window.location.href)
// The following request returns 403 when delegated authentication with an
// OAuthClient is used, as possible scopes do not grant permissions to access the OAuth API:
// See https://github.com/openshift/origin/issues/7011
//
// So little point in trying to delete the token. Lets do in client-side only
//
forceRelogin(currentURI, config)
}

async isLoggedIn(): Promise<boolean> {
return await this.login
}
Expand Down Expand Up @@ -308,7 +329,7 @@ export class OSOAuthService implements OAuthProtoService {

log.info('Log out Openshift')
try {
doLogout(config)
this.doLogout(config)
} catch (error) {
log.error('Error logging out Openshift:', error)
}
Expand Down
13 changes: 1 addition & 12 deletions packages/oauth/src/openshift/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,11 @@ export function buildUserInfoUri(masterUri: string, config: OpenShiftOAuthConfig
return uri.toString()
}

function forceRelogin(url: URL, config: OpenShiftOAuthConfig) {
export function forceRelogin(url: URL, config: OpenShiftOAuthConfig) {
clearTokenStorage()
doLogin(config, { uri: url.toString() })
}

export function doLogout(config: OpenShiftOAuthConfig): void {
const currentURI = new URL(window.location.href)
// The following request returns 403 when delegated authentication with an
// OAuthClient is used, as possible scopes do not grant permissions to access the OAuth API:
// See https://github.com/openshift/origin/issues/7011
//
// So little point in trying to delete the token. Lets do in client-side only
//
forceRelogin(currentURI, config)
}

export function doLogin(config: OpenShiftOAuthConfig, options: { uri: string }): void {
if (!config) {
log.debug('Cannot login due to config now being properly defined')
Expand Down
2 changes: 1 addition & 1 deletion packages/online-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@patternfly/react-styles": "^4.92.6",
"@patternfly/react-table": "^4.113.0",
"@patternfly/react-tokens": "^4.94.6",
"@types/node": "^18.17.18",
"@types/node": "^18.18.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"jquery-match-height": "^0.7.2",
Expand Down
Loading

0 comments on commit 4db3fd5

Please sign in to comment.