Skip to content

Commit

Permalink
Use jwt_decode to get roles on frontend (#1879)
Browse files Browse the repository at this point in the history
* Use jwt_decode to get roles on frontend
  • Loading branch information
conbrad authored Apr 11, 2022
1 parent 1635632 commit d046116
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 22 deletions.
5 changes: 3 additions & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"eslint-plugin-react": "^7.29.4",
"esri-leaflet": "3.0.3",
"filefy": "^0.1.11",
"jwt-decode": "^3.1.2",
"leaflet": "^1.7.1",
"lodash": "^4.17.21",
"luxon": "^2.1.1",
Expand All @@ -65,7 +66,7 @@
"test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!ol)/\"",
"test:ci": "CI=true npm test",
"coverage": "npm test -- --coverage --watchAll=false",
"coverage:ci": "CI=true npm test -- --coverage --watchAll=false",
"coverage:ci": "CI=true REACT_APP_KEYCLOAK_CLIENT=wps-web npm test -- --coverage --watchAll=false",
"cy:open": "cypress open",
"cy:run": "cypress run --config watchForFileChanges=false",
"cypress": "start-server-and-test start:cypress 3030 cy:open",
Expand Down Expand Up @@ -118,4 +119,4 @@
"src/app/store.ts"
]
}
}
}
22 changes: 16 additions & 6 deletions web/src/features/auth/AuthWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'

import { selectAuthentication } from 'app/rootReducer'
import {
authenticate,
setAxiosRequestInterceptors
} from 'features/auth/slices/authenticationSlice'
import { authenticate } from 'features/auth/slices/authenticationSlice'
import axios from 'api/axios'
import { AppThunk } from 'app/store'
import { selectToken, selectAuthentication } from 'app/rootReducer'

interface Props {
shouldAuthenticate: boolean
children: React.ReactElement
}

const setAxiosRequestInterceptors = (): AppThunk => (_, getState) => {
// Use axios interceptors to intercept any requests and add authorization headers.
axios.interceptors.request.use(config => {
const token = selectToken(getState())
if (token) {
config.headers.Authorization = `Bearer ${token}`
}

return config
})
}

const AuthWrapper = ({ children, shouldAuthenticate }: Props) => {
const dispatch = useDispatch()
const { isAuthenticated, authenticating, error } = useSelector(selectAuthentication)
Expand Down
18 changes: 18 additions & 0 deletions web/src/features/auth/slices/authenticationSlice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { decodeRoles } from 'features/auth/slices/authenticationSlice'

describe('authenticationSlice', () => {
it('should return all roles of a user from a token', () => {
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQZUlFYlpQUm5xaTk2ZDl6RFJZT3B5RFBqVHdkei1tcF9kRHJPYmJ4Uko0In0.eyJleHAiOjE2NDk0NTYyMDksImlhdCI6MTY0OTQ1NDQwOSwiYXV0aF90aW1lIjoxNjQ5NDQ3Njg4LCJqdGkiOiI5OGMxZDM1ZC1lYWU4LTQzNTItOWE0Yy04ZTk3ZWY4NTJmMWUiLCJpc3MiOiJodHRwczovL2Rldi5vaWRjLmdvdi5iYy5jYS9hdXRoL3JlYWxtcy84d2w2eDRjcCIsInN1YiI6ImQ4MWI3MTkwLTUzMmItNDNhYi05Y2ZmLWZlOThjMGJlNjNhZSIsInR5cCI6IkJlYXJlciIsImF6cCI6Indwcy13ZWIiLCJub25jZSI6IjIxMWM3NjgzLTQ2M2EtNDMwNC04NThlLWNmYjA0MzA0MjhlMSIsInNlc3Npb25fc3RhdGUiOiIwMGJmNTQ0Mi0yNjliLTQ5MTYtOTAyOS1iMTllMmUyNDM0MmUiLCJhY3IiOiIwIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVzb3VyY2VfYWNjZXNzIjp7Indwcy13ZWIiOnsicm9sZXMiOlsiaGZpX3NlbGVjdF9zdGF0aW9uIiwidGVzdC1yb2xlIiwiaGZpX3NldF9maXJlX3N0YXJ0cyJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJDb25vciBCcmFkeSIsInByZWZlcnJlZF91c2VybmFtZSI6ImNicmFkeUBpZGlyIiwiZ2l2ZW5fbmFtZSI6IkNvbm9yIiwiZmFtaWx5X25hbWUiOiJCcmFkeSIsImVtYWlsIjoiY29ub3IuYnJhZHlAZ292LmJjLmNhIn0.gmJyQqjqtmxwj-eD57cN2_om_5J8GsDlCyeFcEueTMtc_JhKxJDgH90LVUQ0HizdZObpid61cjUJnogb6gyPrzJgesb2FEZaMd88ACU9akbHvYhe4TrBjDPGev5XE9SdMpag8vbVsNa4JIUn6KQxUDhJw8a_olTsqunTT5KKdrPSQyCExk6nDFGE2lZqgDUDIszbpLkzv7xV9T9MOoWeVDJSQvfw3aH4ZXUZ26rxt4RQGDJTInHO6M91zwWPc6Gi0KPxMNv7DG7eyJ8FWg1e8WeptZ6gdcKFgGEIY8lih2TdmeP40gzti1KpBXbMVwLavlXbS46wHgXQTbm-2CW6mQ'

const roles = decodeRoles(token)

expect(roles).toEqual(['hfi_select_station', 'test-role', 'hfi_set_fire_starts'])
})
it('should return no roles if user token has none defined', () => {
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQZUlFYlpQUm5xaTk2ZDl6RFJZT3B5RFBqVHdkei1tcF9kRHJPYmJ4Uko0In0.eyJleHAiOjE2NDk0NjEwMzAsImlhdCI6MTY0OTQ1OTIzMCwiYXV0aF90aW1lIjoxNjQ5NDQ3Njg4LCJqdGkiOiI2Y2IyYTE3Yi1kZTFiLTQ0ZGMtYTFiYy1jODkxM2RiZDYyYmEiLCJpc3MiOiJodHRwczovL2Rldi5vaWRjLmdvdi5iYy5jYS9hdXRoL3JlYWxtcy84d2w2eDRjcCIsInN1YiI6ImQ4MWI3MTkwLTUzMmItNDNhYi05Y2ZmLWZlOThjMGJlNjNhZSIsInR5cCI6IkJlYXJlciIsImF6cCI6Indwcy13ZWIiLCJub25jZSI6ImZmNWI2MjZkLWU3NDktNGM2Yi1iYjk3LWQzOTY0M2MwYzlkYiIsInNlc3Npb25fc3RhdGUiOiIwMGJmNTQ0Mi0yNjliLTQ5MTYtOTAyOS1iMTllMmUyNDM0MmUiLCJhY3IiOiIwIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkNvbm9yIEJyYWR5IiwicHJlZmVycmVkX3VzZXJuYW1lIjoiY2JyYWR5QGlkaXIiLCJnaXZlbl9uYW1lIjoiQ29ub3IiLCJmYW1pbHlfbmFtZSI6IkJyYWR5IiwiZW1haWwiOiJjb25vci5icmFkeUBnb3YuYmMuY2EifQ.eZulBMmLH1QC7p3OOfurV9g0Y1smy29OqULT_JIAx7zeUydaOBIFrsRUbIdZJXqsgtgxlrvFML3sATUfpUj-ujwMTQDE9EZkwT6N_XCrSFD7-jY_-CsiyxofudH_JVEqe98etmSYFFjDf7mxX2smD8s_a7k5exuUJR9Ub9DWIkRXFFsfXKJzJdwpLLmC7ffC2V71PYP9GMRjx4JR2bBdPUyjTP4xQ-dicxAV9w5-EOOPMI79lUQjALi9hlvCWhmBaAmcP5WSlJFvpPvECz3yAON-B_qUJprGATWAbcUEqnxVvyS8I38KfVbkm8WOUPR6FRVhx7s0q_Rq3qwmDDlsXw'
const roles = decodeRoles(token)
expect(roles).toEqual([])
})
})
35 changes: 21 additions & 14 deletions web/src/features/auth/slices/authenticationSlice.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

import axios from 'api/axios'
import { AppThunk } from 'app/store'
import { selectToken } from 'app/rootReducer'
import kcInstance, { kcInitOption } from 'features/auth/keycloak'
import jwt_decode from 'jwt-decode'
import { logError } from 'utils/error'
import { isUndefined } from 'lodash'
import { KC_CLIENT } from 'utils/env'

interface State {
authenticating: boolean
isAuthenticated: boolean
tokenRefreshed: boolean
token: string | undefined
roles: string[]
error: string | null
}

Expand All @@ -19,6 +21,7 @@ export const initialState: State = {
isAuthenticated: false,
tokenRefreshed: false,
token: undefined,
roles: [],
error: null
}

Expand All @@ -39,6 +42,7 @@ const authSlice = createSlice({
state.authenticating = false
state.isAuthenticated = action.payload.isAuthenticated
state.token = action.payload.token
state.roles = decodeRoles(action.payload.token)
},
authenticateError(state: State, action: PayloadAction<string>) {
state.authenticating = false
Expand All @@ -53,6 +57,7 @@ const authSlice = createSlice({
}>
) {
state.token = action.payload.token
state.roles = decodeRoles(action.payload.token)
state.tokenRefreshed = action.payload.tokenRefreshed
}
}
Expand All @@ -67,6 +72,20 @@ const {

export default authSlice.reducer

export const decodeRoles = (token: string | undefined) => {
if (isUndefined(token)) {
return []
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const decodedToken: any = jwt_decode(token)
try {
return decodedToken.resource_access[KC_CLIENT].roles
} catch (e) {
// User has no roles
return []
}
}

export const authenticate = (): AppThunk => dispatch => {
dispatch(authenticateStart())

Expand Down Expand Up @@ -98,15 +117,3 @@ export const authenticate = (): AppThunk => dispatch => {
})
}
}

export const setAxiosRequestInterceptors = (): AppThunk => (_, getState) => {
// Use axios interceptors to intercept any requests and add authorization headers.
axios.interceptors.request.use(config => {
const token = selectToken(getState())
if (token) {
config.headers.Authorization = `Bearer ${token}`
}

return config
})
}
5 changes: 5 additions & 0 deletions web/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10007,6 +10007,11 @@ jss@10.8.2, jss@^10.5.1:
array-includes "^3.1.3"
object.assign "^4.1.2"

jwt-decode@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==

kdbush@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0"
Expand Down

0 comments on commit d046116

Please sign in to comment.