Skip to content

Commit

Permalink
fix(connect): HAWNG-487 apply authentication throttling to connect login
Browse files Browse the repository at this point in the history
  • Loading branch information
tadayosi committed Apr 23, 2024
1 parent 96e0e3c commit 57a035d
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 21 deletions.
41 changes: 32 additions & 9 deletions packages/hawtio/src/plugins/connect/login/ConnectLogin.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { connectService } from '@hawtiosrc/plugins/shared'
import { humanizeSeconds } from '@hawtiosrc/util/dates'
import { Alert, Button, Form, FormAlert, FormGroup, Modal, TextInput } from '@patternfly/react-core'
import React, { useState } from 'react'

Expand All @@ -7,32 +8,54 @@ export const ConnectLogin: React.FunctionComponent = () => {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [loginFailed, setLoginFailed] = useState(false)
const [loginFailedMessage, setLoginFailedMessage] = useState('')
const [isEnabled, setIsEnabled] = useState(true)

const connectionName = connectService.getCurrentConnectionName()
if (!connectionName) {
return null
}

const reset = () => {
setLoginFailed(false)
setLoginFailedMessage('')
setIsEnabled(true)
}

const handleLogin = () => {
const login = async () => {
const ok = await connectService.login(username, password)
if (ok) {
setLoginFailed(false)
// Redirect to the original URL
connectService.redirect()
} else {
setLoginFailed(true)
const result = await connectService.login(username, password)
switch (result.type) {
case 'success':
setLoginFailed(false)
// Redirect to the original URL
connectService.redirect()
break
case 'failure':
setLoginFailed(true)
setLoginFailedMessage('Incorrect username or password')
break
case 'throttled': {
const { retryAfter } = result
setLoginFailed(true)
setLoginFailedMessage(`Login attempt blocked. Retry after ${humanizeSeconds(retryAfter)}`)
setIsEnabled(false)
setTimeout(reset, retryAfter * 1000)
break
}
}
}
login()
}

const handleClose = () => {
setIsOpen(false)
// Closing login modal should also close the window
window.close()
}

const actions = [
<Button key='login' variant='primary' onClick={handleLogin}>
<Button key='login' variant='primary' onClick={handleLogin} isDisabled={!isEnabled}>
Log in
</Button>,
<Button key='cancel' variant='link' onClick={handleClose}>
Expand All @@ -47,7 +70,7 @@ export const ConnectLogin: React.FunctionComponent = () => {
<Form id='connect-login-form' isHorizontal onKeyUp={e => e.key === 'Enter' && handleLogin()}>
{loginFailed && (
<FormAlert>
<Alert variant='danger' title='Incorrect username or password' isInline />
<Alert variant='danger' title={loginFailedMessage} isInline />
</FormAlert>
)}
<FormGroup label='Username' isRequired fieldId='connect-login-form-username'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ConnectionTestResult,
Connections,
IConnectService,
LoginResult,
} from '../connect-service'

class MockConnectService implements IConnectService {
Expand Down Expand Up @@ -53,8 +54,8 @@ class MockConnectService implements IConnectService {
// no-op
}

async login(username: string, password: string): Promise<boolean> {
return false
async login(username: string, password: string): Promise<LoginResult> {
return { type: 'failure' }
}

redirect() {
Expand Down
31 changes: 21 additions & 10 deletions packages/hawtio/src/plugins/shared/connect-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export type ConnectionCredentials = {
password: string
}

export type LoginResult = { type: 'success' } | { type: 'failure' } | { type: 'throttled'; retryAfter: number }

const STORAGE_KEY_CONNECTIONS = 'connect.connections'

const SESSION_KEY_CURRENT_CONNECTION = 'connect.currentConnection'
Expand All @@ -64,7 +66,7 @@ export interface IConnectService {
checkReachable(connection: Connection): Promise<boolean>
testConnection(connection: Connection): Promise<ConnectionTestResult>
connect(connection: Connection): void
login(username: string, password: string): Promise<boolean>
login(username: string, password: string): Promise<LoginResult>
redirect(): void
createJolokia(connection: Connection, checkCredentials?: boolean): Jolokia
getJolokiaUrl(connection: Connection): string
Expand Down Expand Up @@ -242,34 +244,43 @@ class ConnectService implements IConnectService {
/**
* Log in to the current connection.
*/
async login(username: string, password: string): Promise<boolean> {
async login(username: string, password: string): Promise<LoginResult> {
const connection = await this.getCurrentConnection()
if (!connection) {
return false
return { type: 'failure' }
}

// Check credentials
const ok = await new Promise<boolean>(resolve => {
const result = await new Promise<LoginResult>(resolve => {
connection.username = username
connection.password = password
this.createJolokia(connection, true).request(
{ type: 'version' },
{
success: () => resolve(true),
error: () => resolve(false),
ajaxError: () => resolve(false),
success: () => resolve({ type: 'success' }),
error: () => resolve({ type: 'failure' }),
ajaxError: (xhr: JQueryXHR) => {
log.debug('Login error:', xhr.status, xhr.statusText)
if (xhr.status === 429) {
// Login throttled
const retryAfter = parseInt(xhr.getResponseHeader('Retry-After') ?? '0')
resolve({ type: 'throttled', retryAfter })
return
}
resolve({ type: 'failure' })
},
},
)
})
if (!ok) {
return false
if (result.type !== 'success') {
return result
}

// Persist credentials to session storage
await this.setCurrentCredentials({ username, password })
this.clearCredentialsOnLogout()

return true
return result
}

/**
Expand Down

0 comments on commit 57a035d

Please sign in to comment.