Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to VA Azure integration to include claims from backend in VA token #631

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 89 additions & 65 deletions integrations/va-azure/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sign } from '@tsndr/cloudflare-worker-jwt';
import * as jwt from '@tsndr/cloudflare-worker-jwt';
import { Router } from 'itty-router';

import { IntegrationInstallationConfiguration } from '@gitbook/api';
Expand Down Expand Up @@ -35,6 +35,15 @@ type AzureProps = {
};
};

type AzureTokenResponseData = {
access_token?: string;
refresh_token?: string;
token_type: 'Bearer';
expires_in: number;
};

const EXCLUDED_CLAIMS = ['iat', 'exp', 'iss', 'aud', 'jti', 'ver'];

export type AzureAction = { action: 'save.config' };

const configBlock = createComponent<AzureProps, AzureState, AzureAction, AzureRuntimeContext>({
Expand Down Expand Up @@ -204,79 +213,82 @@ const handleFetchEvent: FetchEventCallback<AzureRuntimeContext> = async (request
router.get('/visitor-auth/response', async (request) => {
if ('site' in siteInstallation && siteInstallation.site) {
const publishedContentUrls = await getPublishedContentUrls(context);
const privateKey = context.environment.signingSecrets.siteInstallation!;
let token;
try {
token = await sign(
{ exp: Math.floor(Date.now() / 1000) + 1 * (60 * 60) },
privateKey,
);
} catch (e) {
return new Response('Error: Could not sign JWT token', {
status: 500,
});
}

const tenantId = siteInstallation?.configuration.tenant_id;
const clientId = siteInstallation?.configuration.client_id;
const clientSecret = siteInstallation?.configuration.client_secret;
if (clientId && clientSecret) {
const searchParams = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: `${request.query.code}`,
scope: 'openid',
redirect_uri: `${installationURL}/visitor-auth/response`,

if (!clientId || !clientSecret || !tenantId) {
return new Response(
'Error: Either client id, client secret or tenant id is missing',
{
status: 400,
},
);
}

const searchParams = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: `${request.query.code}`,
redirect_uri: `${installationURL}/visitor-auth/response`,
});
const accessTokenURL = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token/`;
const azureTokenResp = await fetch(accessTokenURL, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: searchParams,
});

if (!azureTokenResp.ok) {
return new Response('Error: Could not fetch token from Azure', {
status: 401,
});
}

const azureTokenData = await azureTokenResp.json<AzureTokenResponseData>();
if (!azureTokenData.access_token) {
return new Response('Error: No Access Token found in response from Okta', {
status: 401,
});
const accessTokenURL = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token/`;
const resp: any = await fetch(accessTokenURL, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: searchParams,
})
.then((response) => response.json())
.catch((err) => {
return new Response('Error: Could not fetch access token from Azure', {
status: 401,
});
}

// Azure already include user/custom claims in the access token so we can just decode it
// TODO: verify token using JWKS and check audience (aud) claims
const decodedOktaToken = await jwt.decode(azureTokenData.access_token);
try {
const privateKey = context.environment.signingSecrets.siteInstallation;
if (!privateKey) {
return new Response('Error: Missing private key from site installation', {
status: 400,
});
}
const jwtToken = await jwt.sign(
{
...sanitizeJWTTokenClaims(decodedOktaToken.payload || {}),
exp: Math.floor(Date.now() / 1000) + 1 * (60 * 60),
},
privateKey,
);

if ('access_token' in resp) {
let url;
if (request.query.state) {
url = new URL(
`${publishedContentUrls?.published}${request.query.state}`,
);
url.searchParams.append('jwt_token', token);
} else {
url = new URL(publishedContentUrls?.published!);
url.searchParams.append('jwt_token', token);
}
if (publishedContentUrls?.published && token) {
return Response.redirect(url.toString());
} else {
return new Response(
"Error: Either JWT token or space's published URL is missing",
{
status: 500,
},
);
}
} else {
logger.debug(JSON.stringify(resp, null, 2));
logger.debug(
`Did not receive access token. Error: ${(resp && resp.error) || ''} ${
(resp && resp.error_description) || ''
}`,
const publishedContentUrl = publishedContentUrls?.published;
if (!publishedContentUrl || !jwtToken) {
return new Response(
"Error: Either JWT token or site's published URL is missing",
{
status: 500,
},
);
return new Response('Error: No Access Token found in response from Azure', {
status: 401,
});
}
} else {
return new Response('Error: Either ClientId or Client Secret is missing', {
status: 400,

const url = new URL(`${publishedContentUrl}${request.query.state || ''}`);
url.searchParams.append('jwt_token', jwtToken);

return Response.redirect(url.toString());
} catch (e) {
return new Response('Error: Could not sign JWT token', {
status: 500,
});
}
}
Expand Down Expand Up @@ -330,3 +342,15 @@ export default createIntegration({
return Response.redirect(url.toString());
},
});

function sanitizeJWTTokenClaims(claims: jwt.JwtPayload) {
const result: Record<string, any> = {};

Object.entries(claims).forEach(([key, value]) => {
if (EXCLUDED_CLAIMS.includes(key)) {
return;
}
result[key] = value;
});
return result;
}