Verify client id and secret in OIDC Federation pkce flow (#2492)

* verify client id and secret for fed id

* support client_secret_basic

* tweaked edit saml fed app to hide and show client secret
This commit is contained in:
Deepak Prabhakara 2024-03-26 21:55:24 +00:00 committed by GitHub
parent 08328e2c42
commit ece4a4fca6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 18 deletions

View File

@ -1,11 +1,17 @@
import { useState } from 'react';
import { Button } from 'react-daisyui';
import type { SAMLFederationApp } from '../types';
import TagsInput from 'react-tagsinput';
import { useTranslation } from 'next-i18next';
import { useFormik } from 'formik';
import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
import EyeSlashIcon from '@heroicons/react/24/outline/EyeSlashIcon';
import { Card } from '../shared';
import { defaultHeaders } from '../utils';
import { ItemList } from '../shared/ItemList';
import { CopyToClipboardButton } from '../shared/InputWithCopyButton';
import { IconButton } from '../shared/IconButton';
type EditApp = Pick<SAMLFederationApp, 'name' | 'acsUrl' | 'tenants' | 'redirectUrl'>;
@ -23,6 +29,7 @@ export const Edit = ({
excludeFields?: 'product'[];
}) => {
const { t } = useTranslation('common');
const [isSecretShown, setisSecretShown] = useState(false);
const connectionIsOIDC = app.type === 'oidc';
const connectionIsSAML = !connectionIsOIDC;
@ -79,9 +86,9 @@ export const Edit = ({
</div>
<input
type='text'
className='input input-bordered w-full text-sm'
className='input input-bordered w-full text-sm bg-gray-100'
value={app.tenant}
disabled
readOnly={true}
/>
</label>
{!excludeFields?.includes('product') && (
@ -91,9 +98,9 @@ export const Edit = ({
</div>
<input
type='text'
className='input input-bordered w-full text-sm'
className='input input-bordered w-full text-sm bg-gray-100'
value={app.product}
disabled
readOnly={true}
/>
</label>
)}
@ -101,12 +108,15 @@ export const Edit = ({
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-fs-client-id')}</span>
<div className='flex'>
<CopyToClipboardButton text={app.clientID!} />
</div>
</div>
<input
type='text'
className='input-bordered input'
className='input-bordered input bg-gray-100'
defaultValue={app.clientID}
disabled
readOnly={true}
/>
</label>
)}
@ -114,12 +124,24 @@ export const Edit = ({
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-fs-client-secret')}</span>
<div className='flex'>
<IconButton
tooltip={isSecretShown ? t('bui-shared-hide') : t('bui-shared-show')}
Icon={isSecretShown ? EyeSlashIcon : EyeIcon}
className='hover:text-primary mr-2'
onClick={(e) => {
e.preventDefault();
setisSecretShown(!isSecretShown);
}}
/>
<CopyToClipboardButton text={app.clientSecret!} />
</div>
</div>
<input
type='text'
className='input-bordered input'
type={isSecretShown ? 'text' : 'password'}
className='input-bordered input bg-gray-100'
defaultValue={app.clientSecret}
disabled
readOnly={true}
/>
</label>
)}
@ -130,9 +152,9 @@ export const Edit = ({
</div>
<input
type='text'
className='input input-bordered w-full text-sm'
className='input input-bordered w-full text-sm bg-gray-100'
value={app.entityId}
disabled
readOnly={true}
/>
<label className='label'>
<span className='label-text-alt'>{t('bui-fs-entity-id-edit-desc')}</span>
@ -175,7 +197,7 @@ export const Edit = ({
onlyUnique={true}
inputProps={{
placeholder: t('bui-fs-enter-tenant'),
autocomplete: 'off',
autoComplete: 'off',
}}
focusedClassName='input-focused'
addOnBlur={true}

View File

@ -226,7 +226,7 @@ export const NewFederatedSAMLApp = ({
onlyUnique={true}
inputProps={{
placeholder: t('bui-fs-enter-tenant'),
autocomplete: 'off',
autoComplete: 'off',
}}
focusedClassName='input-focused'
addOnBlur={true}

View File

@ -174,6 +174,8 @@
"bui-shared-next": "Next",
"bui-shared-previous": "Previous",
"bui-shared-delete": "Delete",
"bui-shared-hide": "Hide",
"bui-shared-show": "Show",
"bui-wku-heading": "Here are the set of URIs you would need access to:",
"bui-wku-idp-configuration-links": "Identity Provider Configuration links",
"bui-wku-desc-idp-configuration": "Links for SAML/OIDC IdP setup",

View File

@ -479,7 +479,14 @@ export class OAuthController implements IOAuthController {
code_challenge,
code_challenge_method,
requested,
oidcFederated: fedApp ? { redirectUrl: fedApp.redirectUrl, id: fedApp.id } : undefined,
oidcFederated: fedApp
? {
redirectUrl: fedApp.redirectUrl,
id: fedApp.id,
clientID: fedApp.clientID,
clientSecret: fedApp.clientSecret,
}
: undefined,
};
await this.sessionStore.put(
sessionId,
@ -1010,11 +1017,24 @@ export class OAuthController implements IOAuthController {
* token_type: bearer
* expires_in: 300
*/
public async token(body: OAuthTokenReq): Promise<OAuthTokenRes> {
public async token(body: OAuthTokenReq, authHeader?: string | null): Promise<OAuthTokenRes> {
let basic_client_id: string | undefined;
let basic_client_secret: string | undefined;
try {
if (authHeader) {
// Authorization: Basic {Base64(<client_id>:<client_secret>)}
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
[basic_client_id, basic_client_secret] = credentials.split(':');
}
} catch (err) {
// no-op
}
const { code, grant_type = 'authorization_code', redirect_uri } = body;
const client_id = 'client_id' in body ? body.client_id : undefined;
const client_secret = 'client_secret' in body ? body.client_secret : undefined;
const code_verifier = 'code_verifier' in body ? body.code_verifier : undefined;
const client_secret = 'client_secret' in body ? body.client_secret : basic_client_id;
const code_verifier = 'code_verifier' in body ? body.code_verifier : basic_client_secret;
metrics.increment('oauthToken');
@ -1050,6 +1070,16 @@ export class OAuthController implements IOAuthController {
if (codeVal.session.code_challenge !== cv) {
throw new JacksonError('Invalid code_verifier', 401);
}
// For Federation flow, we need to verify the client_secret
if (client_id?.startsWith(`${clientIDFederatedPrefix}${clientIDOIDCPrefix}`)) {
if (
client_id !== codeVal.session?.oidcFederated?.clientID ||
client_secret !== codeVal.session?.oidcFederated?.clientSecret
) {
throw new JacksonError('Invalid client_id or client_secret', 401);
}
}
} else if (client_id && client_secret) {
// check if we have an encoded client_id
if (client_id !== 'dummy') {

View File

@ -12,7 +12,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
const { oauthController } = await jackson();
const result = await oauthController.token(req.body);
const authHeader = req.headers['authorization'];
const result = await oauthController.token(req.body, authHeader);
res.json(result);
} catch (err: any) {