Skip to content

Commit

Permalink
feat: adding PKCE in admin-ui authentication #1221
Browse files Browse the repository at this point in the history
  • Loading branch information
duttarnab committed Jul 21, 2023
1 parent b47025c commit e4f87b0
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 9 deletions.
3 changes: 2 additions & 1 deletion admin-ui/app/redux/api/backend-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ export const getUserIpAndLocation = async () => {
}

// Retrieve user information
export const fetchUserInformation = async (code) => {
export const fetchUserInformation = async (code, codeVerifier) => {
return axios
.post('/app/admin-ui/oauth2/user-info', {
code: code,
codeVerifier: codeVerifier,
})
.then((response) => response.data)
.catch((error) => {
Expand Down
17 changes: 15 additions & 2 deletions admin-ui/app/redux/features/authSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const initialState = {
config: {},
backendIsUp: true,
defaultToken: null,
codeChallenge: null,
codeChallengeMethod: 'S256',
codeVerifier: null,
}

const authSlice = createSlice({
Expand Down Expand Up @@ -64,7 +67,15 @@ const authSlice = createSlice({
},
setApiDefaultToken: (state, action) => {
state.defaultToken = action.payload
}
},
getRandomChallengePair: (state, action) => {},
getRandomChallengePairResponse: (state, action) => {
if (action.payload?.codeChallenge) {
state.codeChallenge = action.payload.codeChallenge
state.codeVerifier = action.payload.codeVerifier
localStorage.setItem("codeVerifier", action.payload.codeVerifier)
}
},
}
})

Expand All @@ -79,7 +90,9 @@ export const {
getAPIAccessTokenResponse,
getUserLocation,
getUserLocationResponse,
setApiDefaultToken
setApiDefaultToken,
getRandomChallengePair,
getRandomChallengePairResponse,
} = authSlice.actions
export default authSlice.reducer
reducerRegistry.register('authReducer', authSlice.reducer)
24 changes: 22 additions & 2 deletions admin-ui/app/redux/sagas/AuthSaga.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getUserInfoResponse,
getAPIAccessTokenResponse,
getUserLocationResponse,
getRandomChallengePairResponse,
} from '../features/authSlice'

import {
Expand All @@ -17,6 +18,8 @@ import {
fetchApiTokenWithDefaultScopes,
} from '../api/backend-api'

import {RandomHashGenerator} from 'Utils/RandomHashGenerator'

function* getApiTokenWithDefaultScopes() {
const response = yield call(fetchApiTokenWithDefaultScopes)
return response.access_token
Expand All @@ -36,9 +39,9 @@ function* getOAuth2ConfigWorker({ payload }) {
yield put(getOAuth2ConfigResponse())
}

function* getUserInformationWorker(code) {
function* getUserInformationWorker(code, codeVerifier) {
try {
const response = yield call(fetchUserInformation, code.payload)
const response = yield call(fetchUserInformation, code.payload, localStorage.getItem("codeVerifier"))
if (response) {
yield put(getUserInfoResponse({ uclaims: response.claims, ujwt: response.jwtUserInfo }))
return
Expand All @@ -61,6 +64,19 @@ function* getAPIAccessTokenWorker(jwt) {
}
}

function* getRandomChallengePairWorker() {
try {
const response = yield call(RandomHashGenerator.generateRandomChallengePair, 'SHA-256')
if (response) {
yield put(getRandomChallengePairResponse(response))
return
}

} catch (error) {
console.log('Problems getting API Access Token.', error)
}
}

function* getLocationWorker() {
try {
const response = yield call(getUserIpAndLocation)
Expand Down Expand Up @@ -88,6 +104,9 @@ export function* getOAuth2ConfigWatcher() {
export function* getLocationWatcher() {
yield takeEvery('auth/getUserLocation', getLocationWorker)
}
export function* getRandomChallengePairWatcher() {
yield takeEvery('auth/getRandomChallengePair', getRandomChallengePairWorker)
}

/**
* Auth Root Saga
Expand All @@ -98,5 +117,6 @@ export default function* rootSaga() {
fork(userInfoWatcher),
fork(getApiTokenWatcher),
fork(getLocationWatcher),
fork(getRandomChallengePairWatcher),
])
}
14 changes: 10 additions & 4 deletions admin-ui/app/utils/AppAuthProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getUserInfo,
getAPIAccessToken,
checkLicensePresent,
getRandomChallengePair,
} from 'Redux/actions'
import SessionTimeout from 'Routes/Apps/Gluu/GluuSessionTimeout'
import { checkLicenseConfigValid } from '../redux/actions'
Expand All @@ -19,7 +20,7 @@ export default function AppAuthProvider(props) {
const location = useLocation()
const [showContent, setShowContent] = useState(false)
const [roleNotFound, setRoleNotFound] = useState(false)
const { config, userinfo, userinfo_jwt, token, backendIsUp } = useSelector(
const { config, userinfo, userinfo_jwt, token, backendIsUp, codeChallenge, codeVerifier, codeChallengeMethod } = useSelector(
(state) => state.authReducer
)
const {
Expand All @@ -31,6 +32,7 @@ export default function AppAuthProvider(props) {

useEffect(() => {
dispatch(checkLicenseConfigValid())
dispatch(getRandomChallengePair())
}, [])

useEffect(() => {
Expand Down Expand Up @@ -61,12 +63,16 @@ export default function AppAuthProvider(props) {
!responseType ||
!acrValues ||
!state ||
!nonce
!nonce ||
!codeChallenge ||
!codeVerifier ||
!codeChallengeMethod

) {
console.warn('Parameters to process authz code flow are missing.')
return
}
return `${authzBaseUrl}?acr_values=${acrValues}&response_type=${responseType}&redirect_uri=${redirectUrl}&client_id=${clientId}&scope=${scope}&state=${state}&nonce=${nonce}`
return `${authzBaseUrl}?acr_values=${acrValues}&response_type=${responseType}&redirect_uri=${redirectUrl}&client_id=${clientId}&scope=${scope}&state=${state}&nonce=${nonce}&code_challenge_method=${codeChallengeMethod}&code_challenge=${codeChallenge}`
}

const getDerivedStateFromProps = () => {
Expand All @@ -84,7 +90,7 @@ export default function AppAuthProvider(props) {
if (!userinfo) {
const params = queryString.parse(location.search)
if (params.code && params.scope && params.state) {
dispatch(getUserInfo(params.code))
dispatch(getUserInfo(params.code, codeVerifier))
} else {
if (!showContent && Object.keys(config).length) {
const state = uuidv4()
Expand Down
42 changes: 42 additions & 0 deletions admin-ui/app/utils/RandomHashGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export class RandomHashGenerator {
constructor(randomHashGenerator) {
this.randomHashGenerator = randomHashGenerator
}
static async generateRandomChallengePair(algo) {
const secret = await RandomHashGenerator.generateRandomString();
const encryt = await RandomHashGenerator.encrypt(secret, algo); //'SHA-256'
const hashed = RandomHashGenerator.base64URLEncode(encryt);
return { codeVerifier: secret, codeChallenge: hashed };
}

static base64URLEncode(a) {
var str = "";
var bytes = new Uint8Array(a);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
str += String.fromCharCode(bytes[i]);
}

return btoa(str)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");

}

static dec2hex(dec) {
return ('0' + dec.toString(16)).substr(-2)
}

static generateRandomString() {
var array = new Uint32Array(56 / 2);
window.crypto.getRandomValues(array);
return Array.from(array, RandomHashGenerator.dec2hex).join('');
}

static async encrypt(plain, algo) { // returns promise ArrayBuffer
const encoder = new TextEncoder();
const data = await encoder.encode(plain);
return window.crypto.subtle.digest(algo, data);
}
}

0 comments on commit e4f87b0

Please sign in to comment.