forked from argoproj/argo-cd
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: PKCE authentication flow for web logins argoproj#9890 (argoproj…
…#15889) feat: PKCE authentication flow for web logins argoproj#9890 (argoproj#15889) Signed-off-by: Mayursinh Sarvaiya <marvinduff97@gmail.com> Signed-off-by: Kevin Lyda <kevin@lyda.ie>
- Loading branch information
Showing
16 changed files
with
413 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.pkce-verify { | ||
&__container { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
height: 100vh; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import React, {useEffect, useState} from 'react'; | ||
import {RouteComponentProps} from 'react-router'; | ||
import {services} from '../../shared/services'; | ||
import {PKCECodeVerifier, PKCELoginError, getPKCERedirectURI, pkceCallback} from './utils'; | ||
|
||
import './pkce-verify.scss'; | ||
|
||
export const PKCEVerification = (props: RouteComponentProps<any>) => { | ||
const [loading, setLoading] = useState(true); | ||
const [error, setError] = useState<PKCELoginError | Error>(); | ||
|
||
useEffect(() => { | ||
setLoading(true); | ||
services.authService | ||
.settings() | ||
.then(authSettings => pkceCallback(props.location.search, authSettings.oidcConfig, getPKCERedirectURI().toString())) | ||
.catch(err => setError(err)) | ||
.finally(() => { | ||
setLoading(false); | ||
PKCECodeVerifier.unset(); | ||
}); | ||
}, [props.location]); | ||
|
||
if (loading) { | ||
return <div className='pkce-verify__container'>Processing...</div>; | ||
} | ||
|
||
if (error) { | ||
return ( | ||
<div className='pkce-verify__container'> | ||
<div> | ||
<h3>Error occurred: </h3> | ||
<p>{error?.message || JSON.stringify(error)}</p> | ||
<a href='/login'>Try to Login again</a> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<div className='pkce-verify__container'> | ||
success. if you are not being redirected automatically please <a href='/applications'>click here</a> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { | ||
AuthorizationServer, | ||
Client, | ||
authorizationCodeGrantRequest, | ||
calculatePKCECodeChallenge, | ||
discoveryRequest, | ||
expectNoState, | ||
generateRandomCodeVerifier, | ||
isOAuth2Error, | ||
parseWwwAuthenticateChallenges, | ||
processAuthorizationCodeOpenIDResponse, | ||
processDiscoveryResponse, | ||
validateAuthResponse | ||
} from 'oauth4webapi'; | ||
import {AuthSettings} from '../../shared/models'; | ||
|
||
export const discoverAuthServer = (issuerURL: URL): Promise<AuthorizationServer> => discoveryRequest(issuerURL).then(res => processDiscoveryResponse(issuerURL, res)); | ||
|
||
export const PKCECodeVerifier = { | ||
get: () => sessionStorage.getItem(window.btoa('code_verifier')), | ||
set: (codeVerifier: string) => sessionStorage.setItem(window.btoa('code_verifier'), codeVerifier), | ||
unset: () => sessionStorage.removeItem(window.btoa('code_verifier')) | ||
}; | ||
|
||
export const getPKCERedirectURI = () => { | ||
const currentOrigin = new URL(window.location.origin); | ||
|
||
currentOrigin.pathname = '/pkce/verify'; | ||
|
||
return currentOrigin; | ||
}; | ||
|
||
export class PKCELoginError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
this.name = 'PKCELoginError'; | ||
} | ||
} | ||
|
||
const validateAndGetOIDCForPKCE = async (oidcConfig: AuthSettings['oidcConfig']) => { | ||
if (!oidcConfig) { | ||
throw new PKCELoginError('No OIDC Config found'); | ||
} | ||
|
||
let issuerURL: URL; | ||
try { | ||
issuerURL = new URL(oidcConfig.issuer); | ||
} catch (e) { | ||
throw new PKCELoginError(`Invalid oidc issuer ${oidcConfig.issuer}`); | ||
} | ||
|
||
if (!oidcConfig.clientID) { | ||
throw new PKCELoginError('No OIDC Client Id found'); | ||
} | ||
|
||
let authorizationServer: AuthorizationServer; | ||
try { | ||
authorizationServer = await discoverAuthServer(issuerURL); | ||
} catch (e) { | ||
throw new PKCELoginError(e); | ||
} | ||
|
||
return { | ||
issuerURL, | ||
authorizationServer, | ||
clientID: oidcConfig.clientID | ||
}; | ||
}; | ||
|
||
export const pkceLogin = async (oidcConfig: AuthSettings['oidcConfig'], redirectURI: string) => { | ||
const {authorizationServer} = await validateAndGetOIDCForPKCE(oidcConfig); | ||
|
||
if (!authorizationServer.authorization_endpoint) { | ||
throw new PKCELoginError('No Authorization Server endpoint found'); | ||
} | ||
|
||
if (!authorizationServer?.code_challenge_methods_supported?.includes('S256')) { | ||
throw new PKCELoginError('Authorization Server does not support S256 code challenge method'); | ||
} | ||
|
||
const codeVerifier = generateRandomCodeVerifier(); | ||
|
||
const codeChallange = await calculatePKCECodeChallenge(codeVerifier); | ||
|
||
const authorizationServerConsentScreen = new URL(authorizationServer.authorization_endpoint); | ||
|
||
authorizationServerConsentScreen.searchParams.set('client_id', oidcConfig.clientID); | ||
authorizationServerConsentScreen.searchParams.set('code_challenge', codeChallange); | ||
authorizationServerConsentScreen.searchParams.set('code_challenge_method', 'S256'); | ||
authorizationServerConsentScreen.searchParams.set('redirect_uri', redirectURI); | ||
authorizationServerConsentScreen.searchParams.set('response_type', 'code'); | ||
authorizationServerConsentScreen.searchParams.set('scope', oidcConfig.scopes.join(' ')); | ||
|
||
PKCECodeVerifier.set(codeVerifier); | ||
|
||
window.location.replace(authorizationServerConsentScreen.toString()); | ||
}; | ||
|
||
export const pkceCallback = async (queryParams: string, oidcConfig: AuthSettings['oidcConfig'], redirectURI: string) => { | ||
const codeVerifier = PKCECodeVerifier.get(); | ||
|
||
if (!codeVerifier) { | ||
throw new PKCELoginError('No code verifier found in session'); | ||
} | ||
|
||
let callbackQueryParams = new URLSearchParams(); | ||
try { | ||
callbackQueryParams = new URLSearchParams(queryParams); | ||
} catch (e) { | ||
throw new PKCELoginError('Invalid query parameters'); | ||
} | ||
|
||
if (!callbackQueryParams.get('code')) { | ||
throw new PKCELoginError('No code in query parameters'); | ||
} | ||
|
||
if (callbackQueryParams.get('state') === '') { | ||
callbackQueryParams.delete('state'); | ||
} | ||
|
||
const {authorizationServer} = await validateAndGetOIDCForPKCE(oidcConfig); | ||
|
||
const client: Client = { | ||
client_id: oidcConfig.clientID, | ||
token_endpoint_auth_method: 'none' | ||
}; | ||
|
||
const params = validateAuthResponse(authorizationServer, client, callbackQueryParams, expectNoState); | ||
|
||
if (isOAuth2Error(params)) { | ||
throw new PKCELoginError('Error validating auth response'); | ||
} | ||
|
||
const response = await authorizationCodeGrantRequest(authorizationServer, client, params, redirectURI, codeVerifier); | ||
|
||
const authChallengeExtract = parseWwwAuthenticateChallenges(response); | ||
|
||
if (authChallengeExtract?.length > 0) { | ||
throw new PKCELoginError('Error parsing authentication challenge'); | ||
} | ||
|
||
const result = await processAuthorizationCodeOpenIDResponse(authorizationServer, client, response); | ||
|
||
if (isOAuth2Error(result)) { | ||
throw new PKCELoginError(`Error getting token ${result.error_description}`); | ||
} | ||
|
||
if (!result.id_token) { | ||
throw new PKCELoginError('No token in response'); | ||
} | ||
|
||
document.cookie = `argocd.token=${result.id_token}; path=/`; | ||
|
||
window.location.replace('/applications'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.