Skip to content
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

feat(auth): implement SAML SLO #11599

Merged
merged 5 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/bp/e2e/admin/logout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('Admin - Logout', () => {
await clickOn('#btn-menu-user-dropdown')
await clickOn('#btn-logout')

const response = await getResponse('/api/v2/admin/auth/logout', 'POST')
const response = await getResponse('/api/v2/admin/auth/logout', 'GET')
const headers = response.request().headers()

let profileStatus: number
Expand Down
5 changes: 3 additions & 2 deletions packages/bp/e2e/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ export const logout = async () => {
await clickOn('#btn-menu-user-dropdown')
await clickOn('#btn-logout')

const response = await getResponse('/api/v2/admin/auth/logout', 'POST')
expect(response.status()).toBe(200)
const response = await getResponse('/api/v2/admin/auth/logout', 'GET')
expect(response.status()).toBeGreaterThanOrEqual(200)
expect(response.status()).toBeLessThan(400)

await page.waitForNavigation()
}
Expand Down
4 changes: 2 additions & 2 deletions packages/bp/src/admin/auth/auth-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ class AuthRouter extends CustomAdminRouter {
})
)

router.post(
router.get(
'/logout',
this.checkTokenHeader,
this.asyncMiddleware(async (req: RequestWithUser, res) => {
await this.authService.invalidateToken(req.tokenUser!)
res.sendStatus(200)
return this.authService.logout(req.tokenUser!.strategy, req, res)
})
)

Expand Down
4 changes: 3 additions & 1 deletion packages/bp/src/common/typings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BotDetails, Flow, FlowNode, IO, RolloutStrategy, StageRequestApprovers, StrategyUser } from 'botpress/sdk'
import { Request } from 'express'
import { Request, Response } from 'express'
import { BotpressConfig } from '../core/config/botpress.config'
import { LicenseInfo, LicenseStatus } from './licensing-service'

Expand Down Expand Up @@ -92,6 +92,8 @@ export type RequestWithUser = Request & {
workspace?: string
}

export type LogoutCallback = (strategy: string, req: RequestWithUser, res: Response) => Promise<void>

export interface Bot {
id: string
name: string
Expand Down
27 changes: 25 additions & 2 deletions packages/bp/src/core/security/auth-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { Logger, StrategyUser } from 'botpress/sdk'
import { JWT_COOKIE_NAME } from 'common/auth'
import { AuthPayload, AuthStrategyConfig, ChatUserAuth, TokenUser, TokenResponse } from 'common/typings'
import {
AuthPayload,
AuthStrategyConfig,
ChatUserAuth,
TokenUser,
TokenResponse,
RequestWithUser,
LogoutCallback
} from 'common/typings'
import { TYPES } from 'core/app/types'
import { AuthStrategy, ConfigProvider } from 'core/config'
import Database from 'core/database'
Expand Down Expand Up @@ -28,14 +36,15 @@ export const EXTERNAL_AUTH_HEADER = 'x-bp-externalauth'
export const SERVER_USER = 'server::modules'
const DEFAULT_CHAT_USER_AUTH_DURATION = '24h'

const getUserKey = (email, strategy) => `${email}_${strategy}`
const getUserKey = (email: string, strategy: string) => `${email}_${strategy}`

@injectable()
export class AuthService {
public strategyBasic!: StrategyBasic
private tokenVersions: Dic<number> = {}
private broadcastTokenChange: Function = this.local__tokenVersionChange
public jobService!: JobService
private logoutCallbacks: { [strategy: string]: LogoutCallback } = {}

constructor(
@inject(TYPES.Logger)
Expand Down Expand Up @@ -369,6 +378,20 @@ export class AuthService {
res.cookie(JWT_COOKIE_NAME, token.jwt, { maxAge: token.exp, httpOnly: true, ...cookieOptions })
return true
}

public addLogoutCallback(strategy: string, callback: LogoutCallback) {
this.logoutCallbacks[strategy] = callback
}

public async logout(strategy: string, req: RequestWithUser, res: Response) {
const callback = this.logoutCallbacks[strategy]

if (!callback) {
return res.sendStatus(200)
}

return callback(strategy, req, res)
}
}

export default AuthService
26 changes: 18 additions & 8 deletions packages/ui-shared-lite/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,24 @@ export const tokenNeedsRefresh = () => {
}

export const logout = async (getAxiosClient: () => AxiosInstance) => {
await getAxiosClient()
.post('/admin/auth/logout')
.catch(() => {})

// Clear access token and ID token from local storage
localStorage.removeItem(TOKEN_KEY)
// need to force reload otherwise the token wont clear properly
window.location.href = window.location.origin + window['ROOT_PATH']
let url = ''
try {
const resp = await getAxiosClient().get('/admin/auth/logout')

url = resp.data.url
} catch {
// Silently fails
} finally {
storage.del(TOKEN_KEY)

if (url) {
// If /logout gave us a URL, manually redirect to this URL
window.location.replace(url)
} else {
// need to force reload otherwise the token wont clear properly
window.location.href = window.location.origin + window['ROOT_PATH']
}
}
}

export const setVisitorId = (userId: string, userIdScope?: string) => {
Expand Down