Skip to content

Commit

Permalink
Fix #26 Customization of the OAuth flow page rendering (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch authored Apr 20, 2024
1 parent dc1deb2 commit f81af83
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 207 deletions.
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export * from "./context/context";

export * from "./oauth-app";
export * from "./oauth/authorize-url-generator";
export * from "./oauth/callback";
export * from "./oauth/hook";
export * from "./oauth/escape-html";
export * from "./oauth/installation";
export * from "./oauth/installation-store";
export * from "./oauth/oauth-page-renderer";
export * from "./oauth/state-store";

export * from "./oidc/authorize-url-generator";
export * from "./oidc/callback";
export * from "./oidc/hook";
export * from "./oidc/login";

export * from "./request/request-body";
Expand Down
54 changes: 45 additions & 9 deletions src/oauth-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import {
defaultOAuthStart,
defaultOnFailure,
defaultOnStateValidationError,
} from "./oauth/callback";
} from "./oauth/hook";
import {
OpenIDConnectCallback,
defaultOpenIDConnectCallback,
} from "./oidc/callback";
} from "./oidc/hook";
import { generateOIDCAuthorizeUrl } from "./oidc/authorize-url-generator";
import {
InstallationError,
Expand All @@ -35,6 +35,11 @@ import {
InstallationStoreError,
OpenIDConnectError,
} from "./oauth/error-codes";
import {
OAuthStartPageRenderer,
OAuthCompletionPageRenderer,
OAuthErrorPageRenderer,
} from "./oauth/oauth-page-renderer";

/**
* Options for initializing SlackOAuthApp instance.
Expand Down Expand Up @@ -64,10 +69,15 @@ export interface SlackOAuthAppOptions<E extends SlackOAuthEnv> {
beforeInstallation?: BeforeInstallation;
afterInstallation?: AfterInstallation;
onFailure?: OnFailure;
onFailureRenderer?: OAuthErrorPageRenderer;
onStateValidationError?: OnStateValidationError;
onStateValidationRenderer?: OAuthErrorPageRenderer;
redirectUri?: string;
start?: OAuthStart;
startImmediateRedirect?: boolean; // default: true
startRenderer?: OAuthStartPageRenderer;
callback?: OAuthCallback;
callbackRenderer?: OAuthCompletionPageRenderer;
};

/**
Expand All @@ -78,9 +88,15 @@ export interface SlackOAuthAppOptions<E extends SlackOAuthEnv> {
oidc?: {
stateCookieName?: string;
start?: OAuthStart;
startImmediateRedirect?: boolean; // default: true
startRenderer?: OAuthStartPageRenderer;
// We intentionally don't provide callbackRenderer
// because your app will need to handle the whole response to make it meaningful
callback: OpenIDConnectCallback;
onFailure?: OnFailure;
onFailureRenderer?: OAuthErrorPageRenderer;
onStateValidationError?: OnStateValidationError;
onStateValidationRenderer?: OAuthErrorPageRenderer;
redirectUri?: string;
};

Expand Down Expand Up @@ -168,22 +184,42 @@ export class SlackOAuthApp<E extends SlackOAuthEnv> extends SlackApp<E> {
this.oauth = {
stateCookieName:
options.oauth?.stateCookieName ?? "slack-app-oauth-state",
onFailure: options.oauth?.onFailure ?? defaultOnFailure,
onFailure:
options.oauth?.onFailure ??
defaultOnFailure(options.oauth?.onFailureRenderer),
onStateValidationError:
options.oauth?.onStateValidationError ?? defaultOnStateValidationError,
options.oauth?.onStateValidationError ??
defaultOnStateValidationError(options.oauth?.onStateValidationRenderer),
redirectUri: options.oauth?.redirectUri ?? this.env.SLACK_REDIRECT_URI,
start: options.oauth?.start ?? defaultOAuthStart,
start:
options.oauth?.start ??
defaultOAuthStart(
options.oauth?.startImmediateRedirect,
options.oauth?.startRenderer,
),
beforeInstallation: options.oauth?.beforeInstallation,
afterInstallation: options.oauth?.afterInstallation,
callback: options.oauth?.callback ?? defaultOAuthCallback,
callback:
options.oauth?.callback ??
defaultOAuthCallback(options.oauth?.callbackRenderer),
};
if (options.oidc) {
this.oidc = {
stateCookieName: options.oidc.stateCookieName ?? "slack-app-oidc-state",
onFailure: options.oidc.onFailure ?? defaultOnFailure,
onFailure:
options.oidc.onFailure ??
defaultOnFailure(options.oidc?.onFailureRenderer),
onStateValidationError:
options.oidc.onStateValidationError ?? defaultOnStateValidationError,
start: options.oidc?.start ?? defaultOAuthStart,
options.oidc.onStateValidationError ??
defaultOnStateValidationError(
options.oidc?.onStateValidationRenderer,
),
start:
options.oidc?.start ??
defaultOAuthStart(
options.oidc?.startImmediateRedirect,
options.oidc?.startRenderer,
),
callback: options.oidc.callback ?? defaultOpenIDConnectCallback,
redirectUri:
options.oidc.redirectUri ?? this.env.SLACK_OIDC_REDIRECT_URI,
Expand Down
142 changes: 74 additions & 68 deletions src/oauth/callback.ts → src/oauth/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { OAuthV2AccessResponse } from "slack-web-api-client";
import { InvalidStateParameter, OAuthErrorCode } from "./error-codes";
import { Installation } from "./installation";
import {
renderDefaultCompletionPage,
renderDefaultErrorPage,
renderDefaultStartPage,
OAuthCompletionPageRenderer,
OAuthErrorPageRenderer,
renderDefaultOAuthCompletionPage,
renderDefaultOAuthErrorPage,
renderDefaultOAuthStartPage,
OAuthStartPageRenderer,
} from "./oauth-page-renderer";
import { LoggingEnv } from "../app-env";

Expand Down Expand Up @@ -57,21 +60,24 @@ export type OnStateValidationError = (

/**
* The default onStateValidationError implementation.
* @param startPath the path to start the OAuth flow again
* @returns response
*/
// deno-lint-ignore require-await
export const defaultOnStateValidationError: OnStateValidationError = async ({
startPath,
}) => {
return new Response(
renderDefaultErrorPage(startPath, InvalidStateParameter),
{
status: 400,
headers: { "Content-Type": "text/html; charset=utf-8" },
},
);
};
export function defaultOnStateValidationError(
renderer?: OAuthErrorPageRenderer,
): OnStateValidationError {
return async ({ startPath }) => {
const renderPage = renderer ?? renderDefaultOAuthErrorPage;
return new Response(
await renderPage({
installPath: startPath,
reason: InvalidStateParameter,
}),
{
status: 400,
headers: { "Content-Type": "text/html; charset=utf-8" },
},
);
};
}

/**
* onFailure args
Expand All @@ -94,13 +100,15 @@ export type OnFailure = (args: OnFailureArgs) => Promise<Response>;
* @param reason the error reason code
* @returns response
*/
// deno-lint-ignore require-await
export const defaultOnFailure: OnFailure = async ({ startPath, reason }) => {
return new Response(renderDefaultErrorPage(startPath, reason), {
status: 400,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
};
export function defaultOnFailure(renderer?: OAuthErrorPageRenderer): OnFailure {
return async ({ startPath, reason }) => {
const renderPage = renderer ?? renderDefaultOAuthErrorPage;
return new Response(await renderPage({ installPath: startPath, reason }), {
status: 400,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
};
}

/**
* OAuthStart args
Expand All @@ -120,26 +128,28 @@ export type OAuthStart = (args: OAuthStartArgs) => Promise<Response>;

/**
* The default OAuthStart implementation.
* @param authorizeUrl the authorize URL to redirect the installing user
* @param stateCookieName cookie name used for the state parameter validation
* @param stateValue state parameter string data
* @returns response
*/
// deno-lint-ignore require-await
export const defaultOAuthStart: OAuthStart = async ({
authorizeUrl,
stateCookieName,
stateValue,
}) => {
return new Response(renderDefaultStartPage(authorizeUrl), {
status: 302,
headers: {
Location: authorizeUrl,
"Set-Cookie": `${stateCookieName}=${stateValue}; Secure; HttpOnly; Path=/; Max-Age=300`,
"Content-Type": "text/html; charset=utf-8",
},
});
};
export function defaultOAuthStart(
startImmediateRedirect?: boolean,
renderer?: OAuthStartPageRenderer,
): OAuthStart {
return async ({ authorizeUrl, stateCookieName, stateValue }) => {
const immediateRedirect = startImmediateRedirect !== false;
const status = immediateRedirect ? 302 : 200;
const renderPage = renderer ?? renderDefaultOAuthStartPage;
return new Response(
await renderPage({ immediateRedirect, url: authorizeUrl }),
{
status,
headers: {
Location: authorizeUrl,
"Set-Cookie": `${stateCookieName}=${stateValue}; Secure; HttpOnly; Path=/; Max-Age=300`,
"Content-Type": "text/html; charset=utf-8",
},
},
);
};
}

/**
* OAuthCallback args
Expand All @@ -159,30 +169,26 @@ export type OAuthCallback = (args: OAuthCallbackArgs) => Promise<Response>;

/**
* The default OAuthCallback implementation.
* @param oauthAccess oauth.v2.access API response
* @param enterpriseUrl the management console URL for Enterprise Grid admins
* @param stateCookieName cookie name used for the state parameter validation
* @returns response
*/
// deno-lint-ignore require-await
export const defaultOAuthCallback: OAuthCallback = async ({
oauthAccess,
enterpriseUrl,
stateCookieName,
}) => {
return new Response(
renderDefaultCompletionPage(
oauthAccess.app_id!,
oauthAccess.team?.id!,
oauthAccess.is_enterprise_install,
enterpriseUrl,
),
{
status: 200,
headers: {
"Set-Cookie": `${stateCookieName}=deleted; Secure; HttpOnly; Path=/; Max-Age=0`,
"Content-Type": "text/html; charset=utf-8",
export function defaultOAuthCallback(
renderer?: OAuthCompletionPageRenderer,
): OAuthCallback {
return async ({ oauthAccess, enterpriseUrl, stateCookieName }) => {
const renderPage = renderer ?? renderDefaultOAuthCompletionPage;
return new Response(
await renderPage({
appId: oauthAccess.app_id!,
teamId: oauthAccess.team?.id!,
isEnterpriseInstall: oauthAccess.is_enterprise_install,
enterpriseUrl,
}),
{
status: 200,
headers: {
"Set-Cookie": `${stateCookieName}=deleted; Secure; HttpOnly; Path=/; Max-Age=0`,
"Content-Type": "text/html; charset=utf-8",
},
},
},
);
};
);
};
}
Loading

0 comments on commit f81af83

Please sign in to comment.