mirror of https://github.com/boxyhq/jackson.git
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:
parent
08328e2c42
commit
ece4a4fca6
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue