Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/core/components/auth/auths.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ export default class Auths extends React.Component {
e.preventDefault()

let { authActions, definitions } = this.props
let auths = definitions.map( (val, key) => {
return key
}).toArray()
// `definitions` is an Immutable.Map whose keys are the security scheme
// names we want to log out of. Use `keySeq().toArray()` rather than
// `.map((val, key) => key).toArray()` because `Map#toArray()` returns
// `[key, value]` pairs under Immutable v4+ and values-only under v3,
// so the latter pattern silently produces `[[name, schema]]` instead
// of `[name]` when the host bundles a newer Immutable (see #10761).
let auths = definitions.keySeq().toArray()

this.setState(auths.reduce((prev, auth) => {
prev[auth] = ""
Expand Down
9 changes: 7 additions & 2 deletions src/core/plugins/auth/wrap-actions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @prettier
*/
import { fromJS } from "immutable"
import { fromJS, Map } from "immutable"

/**
* `authorize` and `logout` wrapped actions provide capacity
Expand Down Expand Up @@ -44,7 +44,12 @@ export const logout = (oriAction, system) => (payload) => {
try {
if (configs.persistAuthorization && Array.isArray(payload)) {
payload.forEach((authorizedName) => {
const auth = authorized.get(authorizedName, {})
// Defend against missing entries in the `authorized` store — e.g.
// when a name was never authorized, or when an upstream caller
// passes an unexpected key shape. Use an empty Immutable Map() so
// the following getIn() calls are always safe; plain-object
// fallbacks crash here because getIn is Immutable-specific.
const auth = authorized.get(authorizedName, Map())
const isApiKeyAuth = auth.getIn(["schema", "type"]) === "apiKey"
const isInCookie = auth.getIn(["schema", "in"]) === "cookie"
const isApiKeyInCookie = isApiKeyAuth && isInCookie
Expand Down
12 changes: 7 additions & 5 deletions src/core/plugins/oas31/components/auth/auths.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ class Auths extends React.Component {
e.preventDefault()

let { authActions, definitions } = this.props
let auths = definitions
.map((val, key) => {
return key
})
.toArray()
// `definitions` is an Immutable.Map whose keys are the security scheme
// names we want to log out of. Use `keySeq().toArray()` rather than
// `.map((val, key) => key).toArray()` because `Map#toArray()` returns
// `[key, value]` pairs under Immutable v4+ and values-only under v3,
// so the latter pattern silently produces `[[name, schema]]` instead
// of `[name]` when the host bundles a newer Immutable (see #10761).
let auths = definitions.keySeq().toArray()

this.setState(
auths.reduce((prev, auth) => {
Expand Down
85 changes: 85 additions & 0 deletions test/unit/core/components/auth/auths.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @prettier
*/
import React from "react"
import { shallow } from "enzyme"
import { fromJS, Map } from "immutable"

import Auths from "core/components/auth/auths"

describe("<Auths/> logoutClick", function () {
const dummyComponent = () => null
const components = {
AuthItem: dummyComponent,
oauth2: dummyComponent,
Button: dummyComponent,
}
const getComponentStub = (name) => components[name] || dummyComponent

const buildProps = (definitions) => ({
definitions,
getComponent: getComponentStub,
authSelectors: {
authorized: () => Map(),
},
authActions: {
logoutWithPersistOption: jest.fn(),
},
errSelectors: {},
specSelectors: {},
})

it("sends only the scheme names to logoutWithPersistOption for a single-scheme map", function () {
// The Authorize popup renders one <Auths/> per requirement group,
// passing an Immutable.Map whose keys are the scheme names. The
// Logout handler must emit those keys as a flat array of strings.
const definitions = fromJS({
ApiKeyAuth: { type: "apiKey", in: "header", name: "X-API-Key" },
})
const props = buildProps(definitions)
const wrapper = shallow(<Auths {...props} />)

wrapper.instance().logoutClick({ preventDefault: () => {} })

expect(props.authActions.logoutWithPersistOption).toHaveBeenCalledWith([
"ApiKeyAuth",
])
})

it("sends all scheme names when the map has several entries", function () {
const definitions = fromJS({
ApiKeyAuth: { type: "apiKey", in: "header", name: "X-API-Key" },
BearerAuth: { type: "http", scheme: "bearer" },
})
const props = buildProps(definitions)
const wrapper = shallow(<Auths {...props} />)

wrapper.instance().logoutClick({ preventDefault: () => {} })

expect(props.authActions.logoutWithPersistOption).toHaveBeenCalledWith([
"ApiKeyAuth",
"BearerAuth",
])
})

it("does not leak [key, value] pairs into the payload (guard against Immutable v4 toArray semantics)", function () {
// `Map#toArray()` switched in Immutable v4 from values-only to
// `[key, value]` pairs. The previous `.map((v, k) => k).toArray()`
// pattern therefore emitted `[[name, schema]]` when a host bundled
// Immutable v4+, which broke logout (see #10761). Guard the flat-
// string shape regardless of Immutable version.
const definitions = fromJS({
ApiKeyAuth: { type: "apiKey", in: "header", name: "X-API-Key" },
})
const props = buildProps(definitions)
const wrapper = shallow(<Auths {...props} />)

wrapper.instance().logoutClick({ preventDefault: () => {} })

const [payload] = props.authActions.logoutWithPersistOption.mock.calls[0]
expect(Array.isArray(payload)).toBe(true)
expect(payload).toHaveLength(1)
expect(typeof payload[0]).toBe("string")
expect(payload[0]).toBe("ApiKeyAuth")
})
})
51 changes: 51 additions & 0 deletions test/unit/core/plugins/auth/wrap-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,57 @@ describe("Cookie based apiKey persistence in document.cookie", () => {

expect(document.cookie).toEqual("apiKeyCookie=; Max-Age=-99999999")
})

it("should delete cookie even when payload contains names missing from the authorized store", () => {
// Defensive: if `authorized.get(name)` can't find an entry, the
// fallback must expose `.getIn(...)` safely. A plain-object
// fallback crashes here; an empty Immutable Map() keeps the
// iteration going and still clears cookies for names that are
// present. Guards the fix for
// https://github.com/swagger-api/swagger-ui/issues/10761.
const authorized = fromJS({
api_key: {
schema: {
type: "apiKey",
name: "apiKeyCookie",
in: "cookie",
},
value: "test",
},
})
const system = {
getConfigs: () => ({
persistAuthorization: true,
}),
authSelectors: {
authorized: () => authorized,
},
}
const oriAction = jest.fn()

const logoutAction = () =>
logout(oriAction, system)(["missing_auth", "api_key"])

expect(logoutAction).not.toThrow()
expect(document.cookie).toEqual("apiKeyCookie=; Max-Age=-99999999")
expect(oriAction).toHaveBeenCalledWith(["missing_auth", "api_key"])
})

it("should not throw when every name in payload is missing from the authorized store", () => {
const system = {
getConfigs: () => ({
persistAuthorization: true,
}),
authSelectors: {
authorized: () => fromJS({}),
},
}

const logoutAction = () => logout(jest.fn(), system)(["missing_auth"])

expect(logoutAction).not.toThrow()
expect(document.cookie).toEqual("")
})
})

describe("given persistAuthorization=false", () => {
Expand Down