feat: Preserve authorization on browser refresh and close/reopen (#5939)
* Add default configuration `preserveAuthorization` * Add localStorage to auth plugin * Add persistAuthorization unit tests * Refactor persistAuthorization to use wrapped actions * Upgrade unit tests to be compatible with jest * Add persistAuthorization documentation Co-authored-by: Tim Lai <timothy.lai@gmail.com>
This commit is contained in:
committed by
GitHub
parent
48ee32faa1
commit
96aecc8860
@@ -84,6 +84,12 @@ Parameter name | Docker variable | Description
|
|||||||
<a name="modelPropertyMacro"></a>`modelPropertyMacro` | _Unavailable_ | `Function`. Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable
|
<a name="modelPropertyMacro"></a>`modelPropertyMacro` | _Unavailable_ | `Function`. Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable
|
||||||
<a name="parameterMacro"></a>`parameterMacro` | _Unavailable_ | `Function`. Function to set default value to parameters. Accepts two arguments parameterMacro(operation, parameter). Operation and parameter are objects passed for context, both remain immutable
|
<a name="parameterMacro"></a>`parameterMacro` | _Unavailable_ | `Function`. Function to set default value to parameters. Accepts two arguments parameterMacro(operation, parameter). Operation and parameter are objects passed for context, both remain immutable
|
||||||
|
|
||||||
|
##### Authorization
|
||||||
|
|
||||||
|
Parameter name | Docker variable | Description
|
||||||
|
--- | --- | -----
|
||||||
|
<a name="persistAuthorization"></a>`persistAuthorization` | _Unavailable_ | `Boolean=false`. If set to `true`, it persists authorization data and it would not be lost on browser close/refresh
|
||||||
|
|
||||||
### Instance methods
|
### Instance methods
|
||||||
|
|
||||||
**💡 Take note! These are methods, not parameters**.
|
**💡 Take note! These are methods, not parameters**.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default class Auths extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
let { authActions } = this.props
|
let { authActions } = this.props
|
||||||
authActions.authorize(this.state)
|
authActions.authorizeWithPersistOption(this.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutClick =(e) => {
|
logoutClick =(e) => {
|
||||||
@@ -43,7 +43,7 @@ export default class Auths extends React.Component {
|
|||||||
return prev
|
return prev
|
||||||
}, {}))
|
}, {}))
|
||||||
|
|
||||||
authActions.logout(auths)
|
authActions.logoutWithPersistOption(auths)
|
||||||
}
|
}
|
||||||
|
|
||||||
close =(e) => {
|
close =(e) => {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export default class Oauth2 extends React.Component {
|
|||||||
let { authActions, errActions, name } = this.props
|
let { authActions, errActions, name } = this.props
|
||||||
|
|
||||||
errActions.clear({authId: name, type: "auth", source: "auth"})
|
errActions.clear({authId: name, type: "auth", source: "auth"})
|
||||||
authActions.logout([ name ])
|
authActions.logoutWithPersistOption([ name ])
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default function SwaggerUI(opts) {
|
|||||||
filter: null,
|
filter: null,
|
||||||
validatorUrl: "https://validator.swagger.io/validator",
|
validatorUrl: "https://validator.swagger.io/validator",
|
||||||
oauth2RedirectUrl: `${window.location.protocol}//${window.location.host}/oauth2-redirect.html`,
|
oauth2RedirectUrl: `${window.location.protocol}//${window.location.host}/oauth2-redirect.html`,
|
||||||
|
persistAuthorization: false,
|
||||||
configs: {},
|
configs: {},
|
||||||
custom: {},
|
custom: {},
|
||||||
displayOperationId: false,
|
displayOperationId: false,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const PRE_AUTHORIZE_OAUTH2 = "pre_authorize_oauth2"
|
|||||||
export const AUTHORIZE_OAUTH2 = "authorize_oauth2"
|
export const AUTHORIZE_OAUTH2 = "authorize_oauth2"
|
||||||
export const VALIDATE = "validate"
|
export const VALIDATE = "validate"
|
||||||
export const CONFIGURE_AUTH = "configure_auth"
|
export const CONFIGURE_AUTH = "configure_auth"
|
||||||
|
export const RESTORE_AUTHORIZATION = "restore_authorization"
|
||||||
|
|
||||||
const scopeSeparator = " "
|
const scopeSeparator = " "
|
||||||
|
|
||||||
@@ -26,6 +27,11 @@ export function authorize(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const authorizeWithPersistOption = (payload) => ( { authActions } ) => {
|
||||||
|
authActions.authorize(payload)
|
||||||
|
authActions.persistAuthorizationIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
export function logout(payload) {
|
export function logout(payload) {
|
||||||
return {
|
return {
|
||||||
type: LOGOUT,
|
type: LOGOUT,
|
||||||
@@ -33,6 +39,11 @@ export function logout(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const logoutWithPersistOption = (payload) => ( { authActions } ) => {
|
||||||
|
authActions.logout(payload)
|
||||||
|
authActions.persistAuthorizationIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
export const preAuthorizeImplicit = (payload) => ( { authActions, errActions } ) => {
|
export const preAuthorizeImplicit = (payload) => ( { authActions, errActions } ) => {
|
||||||
let { auth , token, isValid } = payload
|
let { auth , token, isValid } = payload
|
||||||
let { schema, name } = auth
|
let { schema, name } = auth
|
||||||
@@ -60,9 +71,10 @@ export const preAuthorizeImplicit = (payload) => ( { authActions, errActions } )
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authActions.authorizeOauth2({ auth, token })
|
authActions.authorizeOauth2WithPersistOption({ auth, token })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function authorizeOauth2(payload) {
|
export function authorizeOauth2(payload) {
|
||||||
return {
|
return {
|
||||||
type: AUTHORIZE_OAUTH2,
|
type: AUTHORIZE_OAUTH2,
|
||||||
@@ -70,6 +82,12 @@ export function authorizeOauth2(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const authorizeOauth2WithPersistOption = (payload) => ( { authActions } ) => {
|
||||||
|
authActions.authorizeOauth2(payload)
|
||||||
|
authActions.persistAuthorizationIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
export const authorizePassword = ( auth ) => ( { authActions } ) => {
|
export const authorizePassword = ( auth ) => ( { authActions } ) => {
|
||||||
let { schema, name, username, password, passwordType, clientId, clientSecret } = auth
|
let { schema, name, username, password, passwordType, clientId, clientSecret } = auth
|
||||||
let form = {
|
let form = {
|
||||||
@@ -208,7 +226,7 @@ export const authorizeRequest = ( data ) => ( { fn, getConfigs, authActions, err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authActions.authorizeOauth2({ auth, token})
|
authActions.authorizeOauth2WithPersistOption({ auth, token})
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
let err = new Error(e)
|
let err = new Error(e)
|
||||||
@@ -244,3 +262,19 @@ export function configureAuth(payload) {
|
|||||||
payload: payload
|
payload: payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function restoreAuthorization(payload) {
|
||||||
|
return {
|
||||||
|
type: RESTORE_AUTHORIZATION,
|
||||||
|
payload: payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const persistAuthorizationIfNeeded = () => ( { authSelectors, getConfigs } ) => {
|
||||||
|
const configs = getConfigs()
|
||||||
|
if (configs.persistAuthorization)
|
||||||
|
{
|
||||||
|
const authorized = authSelectors.authorized()
|
||||||
|
localStorage.setItem("authorized", JSON.stringify(authorized.toJS()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
AUTHORIZE,
|
AUTHORIZE,
|
||||||
AUTHORIZE_OAUTH2,
|
AUTHORIZE_OAUTH2,
|
||||||
LOGOUT,
|
LOGOUT,
|
||||||
CONFIGURE_AUTH
|
CONFIGURE_AUTH,
|
||||||
|
RESTORE_AUTHORIZATION
|
||||||
} from "./actions"
|
} from "./actions"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -50,7 +51,10 @@ export default {
|
|||||||
auth.token = Object.assign({}, token)
|
auth.token = Object.assign({}, token)
|
||||||
parsedAuth = fromJS(auth)
|
parsedAuth = fromJS(auth)
|
||||||
|
|
||||||
return state.setIn( [ "authorized", parsedAuth.get("name") ], parsedAuth )
|
let map = state.get("authorized") || Map()
|
||||||
|
map = map.set(parsedAuth.get("name"), parsedAuth)
|
||||||
|
|
||||||
|
return state.set( "authorized", map )
|
||||||
},
|
},
|
||||||
|
|
||||||
[LOGOUT]: (state, { payload } ) =>{
|
[LOGOUT]: (state, { payload } ) =>{
|
||||||
@@ -65,5 +69,9 @@ export default {
|
|||||||
|
|
||||||
[CONFIGURE_AUTH]: (state, { payload } ) =>{
|
[CONFIGURE_AUTH]: (state, { payload } ) =>{
|
||||||
return state.set("configs", payload)
|
return state.set("configs", payload)
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[RESTORE_AUTHORIZATION]: (state, { payload } ) =>{
|
||||||
|
return state.set("authorized", fromJS(payload.authorized))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,17 @@ export function toggle(configName) {
|
|||||||
|
|
||||||
|
|
||||||
// Hook
|
// Hook
|
||||||
export const loaded = () => () => {}
|
export const loaded = () => ({getConfigs, authActions}) => {
|
||||||
|
// check if we should restore authorization data from localStorage
|
||||||
|
const configs = getConfigs()
|
||||||
|
if (configs.persistAuthorization)
|
||||||
|
{
|
||||||
|
const authorized = localStorage.getItem("authorized")
|
||||||
|
if(authorized)
|
||||||
|
{
|
||||||
|
authActions.restoreAuthorization({
|
||||||
|
authorized: JSON.parse(authorized)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
|
import { Map } from "immutable"
|
||||||
import { authorizeRequest, authorizeAccessCodeWithFormParams } from "corePlugins/auth/actions"
|
import {
|
||||||
|
authorizeRequest,
|
||||||
|
authorizeAccessCodeWithFormParams,
|
||||||
|
authorizeWithPersistOption,
|
||||||
|
authorizeOauth2WithPersistOption,
|
||||||
|
logoutWithPersistOption,
|
||||||
|
persistAuthorizationIfNeeded
|
||||||
|
} from "corePlugins/auth/actions"
|
||||||
|
|
||||||
describe("auth plugin - actions", () => {
|
describe("auth plugin - actions", () => {
|
||||||
|
|
||||||
@@ -184,4 +191,123 @@ describe("auth plugin - actions", () => {
|
|||||||
expect(actualArgument.body).toContain("grant_type=authorization_code")
|
expect(actualArgument.body).toContain("grant_type=authorization_code")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("persistAuthorization", () => {
|
||||||
|
describe("wrapped functions with persist option", () => {
|
||||||
|
it("should wrap `authorize` action and persist data if needed", () => {
|
||||||
|
|
||||||
|
// Given
|
||||||
|
const data = {
|
||||||
|
"api_key": {}
|
||||||
|
}
|
||||||
|
const system = {
|
||||||
|
getConfigs: () => ({}),
|
||||||
|
authActions: {
|
||||||
|
authorize: jest.fn(()=>{}),
|
||||||
|
persistAuthorizationIfNeeded: jest.fn(()=>{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When
|
||||||
|
authorizeWithPersistOption(data)(system)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(system.authActions.authorize).toHaveBeenCalled()
|
||||||
|
expect(system.authActions.authorize).toHaveBeenCalledWith(data)
|
||||||
|
expect(system.authActions.persistAuthorizationIfNeeded).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should wrap `oauth2Authorize` action and persist data if needed", () => {
|
||||||
|
|
||||||
|
// Given
|
||||||
|
const data = {
|
||||||
|
"api_key": {}
|
||||||
|
}
|
||||||
|
const system = {
|
||||||
|
getConfigs: () => ({}),
|
||||||
|
authActions: {
|
||||||
|
authorizeOauth2: jest.fn(()=>{}),
|
||||||
|
persistAuthorizationIfNeeded: jest.fn(()=>{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When
|
||||||
|
authorizeOauth2WithPersistOption(data)(system)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(system.authActions.authorizeOauth2).toHaveBeenCalled()
|
||||||
|
expect(system.authActions.authorizeOauth2).toHaveBeenCalledWith(data)
|
||||||
|
expect(system.authActions.persistAuthorizationIfNeeded).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should wrap `logout` action and persist data if needed", () => {
|
||||||
|
|
||||||
|
// Given
|
||||||
|
const data = {
|
||||||
|
"api_key": {}
|
||||||
|
}
|
||||||
|
const system = {
|
||||||
|
getConfigs: () => ({}),
|
||||||
|
authActions: {
|
||||||
|
logout: jest.fn(()=>{}),
|
||||||
|
persistAuthorizationIfNeeded: jest.fn(()=>{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When
|
||||||
|
logoutWithPersistOption(data)(system)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(system.authActions.logout).toHaveBeenCalled()
|
||||||
|
expect(system.authActions.logout).toHaveBeenCalledWith(data)
|
||||||
|
expect(system.authActions.persistAuthorizationIfNeeded).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("persistAuthorizationIfNeeded", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
it("should skip if `persistAuthorization` is turned off", () => {
|
||||||
|
// Given
|
||||||
|
const system = {
|
||||||
|
getConfigs: () => ({
|
||||||
|
persistAuthorization: false
|
||||||
|
}),
|
||||||
|
authSelectors: {
|
||||||
|
authorized: jest.fn(()=>{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When
|
||||||
|
persistAuthorizationIfNeeded()(system)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(system.authSelectors.authorized).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
it("should persist authorization data to localStorage", () => {
|
||||||
|
// Given
|
||||||
|
const data = {
|
||||||
|
"api_key": {}
|
||||||
|
}
|
||||||
|
const system = {
|
||||||
|
getConfigs: () => ({
|
||||||
|
persistAuthorization: true
|
||||||
|
}),
|
||||||
|
authSelectors: {
|
||||||
|
authorized: jest.fn(()=>Map(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jest.spyOn(Object.getPrototypeOf(window.localStorage), "setItem")
|
||||||
|
|
||||||
|
// When
|
||||||
|
persistAuthorizationIfNeeded()(system)
|
||||||
|
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalled()
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith("authorized", JSON.stringify(data))
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { downloadConfig } from "corePlugins/configs/spec-actions"
|
import { downloadConfig } from "corePlugins/configs/spec-actions"
|
||||||
|
import { loaded } from "corePlugins/configs/actions"
|
||||||
|
|
||||||
describe("configs plugin - actions", () => {
|
describe("configs plugin - actions", () => {
|
||||||
|
|
||||||
@@ -23,4 +23,43 @@ describe("configs plugin - actions", () => {
|
|||||||
expect(fetchSpy).toHaveBeenCalledWith(req)
|
expect(fetchSpy).toHaveBeenCalledWith(req)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("loaded hook", () => {
|
||||||
|
describe("authorization data restoration", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
it("retrieve `authorized` value from `localStorage`", () => {
|
||||||
|
const system = {
|
||||||
|
getConfigs: () => ({
|
||||||
|
persistAuthorization: true
|
||||||
|
}),
|
||||||
|
authActions: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jest.spyOn(Object.getPrototypeOf(window.localStorage), "getItem")
|
||||||
|
loaded()(system)
|
||||||
|
expect(localStorage.getItem).toHaveBeenCalled()
|
||||||
|
expect(localStorage.getItem).toHaveBeenCalledWith("authorized")
|
||||||
|
})
|
||||||
|
it("restore authorization data when a value exists", () => {
|
||||||
|
const system = {
|
||||||
|
getConfigs: () => ({
|
||||||
|
persistAuthorization: true
|
||||||
|
}),
|
||||||
|
authActions: {
|
||||||
|
restoreAuthorization: jest.fn(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mockData = {"api_key": {}}
|
||||||
|
localStorage.setItem("authorized", JSON.stringify(mockData))
|
||||||
|
loaded()(system)
|
||||||
|
expect(system.authActions.restoreAuthorization).toHaveBeenCalled()
|
||||||
|
expect(system.authActions.restoreAuthorization).toHaveBeenCalledWith({
|
||||||
|
authorized: mockData
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user