diff --git a/README.md b/README.md
index 65f9c02..19d7ea6 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ configurations.
// server.ts
import {
createGitHubOAuthConfig,
- getSessionId,
+ getSessionData,
handleCallback,
signIn,
signOut,
@@ -67,18 +67,27 @@ configurations.
const oauthConfig = createGitHubOAuthConfig();
+ async function getGitHubUser(accessToken: string) {
+ const response = await fetch("https://api.github.com/user", {
+ headers: {
+ Authorization: `bearer ${accessToken}`,
+ },
+ });
+ if (!response.ok) throw new Error("Failed to fetch GitHub user profile");
+ return await response.json();
+ }
+
async function handler(request: Request) {
const { pathname } = new URL(request.url);
switch (pathname) {
case "/oauth/signin":
return await signIn(request, oauthConfig);
case "/oauth/callback":
- const { response } = await handleCallback(request, oauthConfig);
- return response;
+ return await handleCallback(request, oauthConfig, getGitHubUser);
case "/oauth/signout":
return await signOut(request);
case "/protected-route":
- return await getSessionId(request) === undefined
+ return await getSessionData(request) === null
? new Response("Unauthorized", { status: 401 })
: new Response("You are allowed");
default:
@@ -110,7 +119,7 @@ configurations.
// server.ts
import {
getRequiredEnv,
- getSessionId,
+ getSessionData,
handleCallback,
type OAuth2ClientConfig,
signIn,
@@ -125,18 +134,27 @@ configurations.
redirectUri: "https://my-site.com/another-dir/callback",
};
+ async function getGitHubUser(accessToken: string) {
+ const response = await fetch("https://api.github.com/user", {
+ headers: {
+ Authorization: `bearer ${accessToken}`,
+ },
+ });
+ if (!response.ok) throw new Error("Failed to fetch GitHub user profile");
+ return await response.json();
+ }
+
async function handler(request: Request) {
const { pathname } = new URL(request.url);
switch (pathname) {
case "/oauth/signin":
return await signIn(request, oauthConfig);
case "/another-dir/callback":
- const { response } = await handleCallback(request, oauthConfig);
- return response;
+ return await handleCallback(request, oauthConfig, getGitHubUser);
case "/oauth/signout":
return await signOut(request);
case "/protected-route":
- return await getSessionId(request) === undefined
+ return await getSessionData(request) === null
? new Response("Unauthorized", { status: 401 })
: new Response("You are allowed");
default:
@@ -174,7 +192,7 @@ This is required for OAuth solutions that span more than one sub-domain.
signIn,
handleCallback,
signOut,
- getSessionId,
+ getSessionData,
} = createHelpers(createGitHubOAuthConfig(), {
cookieOptions: {
name: "__Secure-triple-choc",
@@ -182,18 +200,27 @@ This is required for OAuth solutions that span more than one sub-domain.
},
});
+ async function getGitHubUser(accessToken: string) {
+ const response = await fetch("https://api.github.com/user", {
+ headers: {
+ Authorization: `bearer ${accessToken}`,
+ },
+ });
+ if (!response.ok) throw new Error("Failed to fetch GitHub user profile");
+ return await response.json();
+ }
+
async function handler(request: Request) {
const { pathname } = new URL(request.url);
switch (pathname) {
case "/oauth/signin":
return await signIn(request);
case "/oauth/callback":
- const { response } = await handleCallback(request);
- return response;
+ return await handleCallback(request, getGitHubUser);
case "/oauth/signout":
return await signOut(request);
case "/protected-route":
- return await getSessionId(request) === undefined
+ return await getSessionData(request) === null
? new Response("Unauthorized", { status: 401 })
: new Response("You are allowed");
default:
@@ -225,10 +252,20 @@ This is required for OAuth solutions that span more than one sub-domain.
} from "https://deno.land/x/deno_kv_oauth/mod.ts";
import type { Plugin } from "$fresh/server.ts";
- const { signIn, handleCallback, signOut, getSessionId } = createHelpers(
+ const { signIn, handleCallback, signOut, getSessionData } = createHelpers(
createGitHubOAuthConfig(),
);
+ async function getGitHubUser(accessToken: string) {
+ const response = await fetch("https://api.github.com/user", {
+ headers: {
+ Authorization: `bearer ${accessToken}`,
+ },
+ });
+ if (!response.ok) throw new Error("Failed to fetch GitHub user profile");
+ return await response.json();
+ }
+
export default {
name: "kv-oauth",
routes: [
@@ -241,9 +278,7 @@ This is required for OAuth solutions that span more than one sub-domain.
{
path: "/callback",
async handler(req) {
- // Return object also includes `accessToken` and `sessionId` properties.
- const { response } = await handleCallback(req);
- return response;
+ return await handleCallback(req, getGitHubUser);
},
},
{
@@ -255,7 +290,7 @@ This is required for OAuth solutions that span more than one sub-domain.
{
path: "/protected",
async handler(req) {
- return await getSessionId(req) === undefined
+ return await getSessionData(req) === null
? new Response("Unauthorized", { status: 401 })
: new Response("You are allowed");
},
diff --git a/demo.ts b/demo.ts
index 846fc9d..df06b39 100644
--- a/demo.ts
+++ b/demo.ts
@@ -1,12 +1,6 @@
// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
import { STATUS_CODE } from "./deps.ts";
-import {
- createGitHubOAuthConfig,
- getSessionId,
- handleCallback,
- signIn,
- signOut,
-} from "./mod.ts";
+import { createGitHubOAuthConfig, createHelpers } from "./mod.ts";
/**
* Modify the OAuth configuration creation function when testing for providers.
@@ -19,16 +13,22 @@ import {
* ```
*/
const oauthConfig = createGitHubOAuthConfig();
+const { getSessionData, handleCallback, signIn, signOut } = createHelpers<
+ GitHubUser
+>(
+ oauthConfig,
+);
async function indexHandler(request: Request) {
- const sessionId = await getSessionId(request);
- const hasSessionIdCookie = sessionId !== undefined;
+ const sessionData = await getSessionData(request);
+ const hasSessionIdCookie = sessionData !== null;
const body = `
Authorization endpoint URI: ${oauthConfig.authorizationEndpointUri}
Token URI: ${oauthConfig.tokenUri}
Scope: ${oauthConfig.defaults?.scope}
Signed in: ${hasSessionIdCookie}
+ ${JSON.stringify(sessionData, null, 2)}
Sign in
@@ -45,6 +45,29 @@ async function indexHandler(request: Request) {
});
}
+interface GitHubUser {
+ login: string;
+ avatarUrl: string;
+}
+
+async function getGitHubUser(accessToken: string) {
+ const response = await fetch("https://api.github.com/user", {
+ headers: {
+ authorization: `token ${accessToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to get GitHub user");
+ }
+
+ const data = await response.json();
+ return {
+ login: data.login,
+ avatarUrl: data.avatar_url,
+ } as GitHubUser;
+}
+
export async function handler(request: Request): Promise {
if (request.method !== "GET") {
return new Response(null, { status: STATUS_CODE.NotFound });
@@ -55,12 +78,14 @@ export async function handler(request: Request): Promise {
return await indexHandler(request);
}
case "/signin": {
- return await signIn(request, oauthConfig);
+ return await signIn(request);
}
case "/callback": {
try {
- const { response } = await handleCallback(request, oauthConfig);
- return response;
+ return await handleCallback(
+ request,
+ getGitHubUser,
+ );
} catch {
return new Response(null, { status: STATUS_CODE.InternalServerError });
}
diff --git a/lib/_kv.ts b/lib/_kv.ts
index c73cf57..a689562 100644
--- a/lib/_kv.ts
+++ b/lib/_kv.ts
@@ -56,24 +56,21 @@ export async function setOAuthSession(
await kv.set(oauthSessionKey(id), value, options);
}
-/**
- * The site session is created on the server. It is stored in the database to
- * later validate that a session was created on the server. It has no purpose
- * beyond that. Hence, the value of the site session entry is arbitrary.
- */
-type SiteSession = true;
-
function siteSessionKey(id: string): [string, string] {
return ["site_sessions", id];
}
-export async function isSiteSession(id: string): Promise {
- const res = await kv.get(siteSessionKey(id));
- return res.value !== null;
+export async function getSiteSession(id: string): Promise {
+ const res = await kv.get(siteSessionKey(id));
+ return res.value;
}
-export async function setSiteSession(id: string, expireIn?: number) {
- await kv.set(siteSessionKey(id), true, { expireIn });
+export async function setSiteSession(
+ id: string,
+ value?: unknown,
+ expireIn?: number,
+) {
+ await kv.set(siteSessionKey(id), value, { expireIn });
}
export async function deleteSiteSession(id: string) {
diff --git a/lib/_kv_test.ts b/lib/_kv_test.ts
index dfc4ca9..00232cb 100644
--- a/lib/_kv_test.ts
+++ b/lib/_kv_test.ts
@@ -1,6 +1,12 @@
// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals, assertRejects } from "std/assert/mod.ts";
-import { getAndDeleteOAuthSession, setOAuthSession } from "./_kv.ts";
+import {
+ deleteSiteSession,
+ getAndDeleteOAuthSession,
+ getSiteSession,
+ setOAuthSession,
+ setSiteSession,
+} from "./_kv.ts";
import { randomOAuthSession } from "./_test_utils.ts";
Deno.test("(getAndDelete/set)OAuthSession()", async () => {
@@ -22,3 +28,16 @@ Deno.test("(getAndDelete/set)OAuthSession()", async () => {
"OAuth session not found",
);
});
+
+Deno.test("(get/set/delete)SiteSession()", async () => {
+ const id = crypto.randomUUID();
+
+ assertEquals(await getSiteSession(id), null);
+
+ const siteSession = { foo: "bar" };
+ await setSiteSession(id, siteSession);
+ assertEquals(await getSiteSession(id), siteSession);
+
+ await deleteSiteSession(id);
+ assertEquals(await getSiteSession(id), null);
+});
diff --git a/lib/create_helpers.ts b/lib/create_helpers.ts
index 24395ff..30ac558 100644
--- a/lib/create_helpers.ts
+++ b/lib/create_helpers.ts
@@ -1,7 +1,7 @@
// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
-import { type Cookie, OAuth2ClientConfig, type Tokens } from "../deps.ts";
-import { getSessionId } from "./get_session_id.ts";
+import { type Cookie, OAuth2ClientConfig } from "../deps.ts";
+import { getSessionData } from "./get_session_data.ts";
import { handleCallback } from "./handle_callback.ts";
import { signIn, type SignInOptions } from "./sign_in.ts";
import { signOut } from "./sign_out.ts";
@@ -31,7 +31,7 @@ export interface CreateHelpersOptions {
* signIn,
* handleCallback,
* signOut,
- * getSessionId,
+ * getSessionData,
* } = createHelpers(createGitHubOAuthConfig(), {
* cookieOptions: {
* name: "__Secure-triple-choc",
@@ -39,18 +39,27 @@ export interface CreateHelpersOptions {
* },
* });
*
+ * async function getGitHubUser(accessToken: string) {
+ * const response = await fetch("https://api.github.com/user", {
+ * headers: {
+ * Authorization: `bearer ${accessToken}`,
+ * },
+ * });
+ * if (!response.ok) throw new Error("Failed to fetch GitHub user profile");
+ * return await response.json();
+ * }
+ *
* async function handler(request: Request) {
* const { pathname } = new URL(request.url);
* switch (pathname) {
* case "/oauth/signin":
* return await signIn(request);
* case "/oauth/callback":
- * const { response } = await handleCallback(request);
- * return response;
+ * return await handleCallback(request, getGitHubUser);
* case "/oauth/signout":
* return await signOut(request);
* case "/protected-route":
- * return await getSessionId(request) === undefined
+ * return await getSessionData(request) === null
* ? new Response("Unauthorized", { status: 401 })
* : new Response("You are allowed");
* default:
@@ -61,33 +70,36 @@ export interface CreateHelpersOptions {
* Deno.serve(handler);
* ```
*/
-export function createHelpers(
+// deno-lint-ignore ban-types
+export function createHelpers = {}>(
oauthConfig: OAuth2ClientConfig,
options?: CreateHelpersOptions,
): {
signIn(request: Request, options?: SignInOptions): Promise;
- handleCallback(request: Request): Promise<{
- response: Response;
- sessionId: string;
- tokens: Tokens;
- }>;
+ handleCallback(
+ request: Request,
+ tokenHandler: (accessToken: string) => T | Promise,
+ ): Promise;
signOut(request: Request): Promise;
- getSessionId(request: Request): Promise;
+ getSessionData(request: Request): Promise;
} {
return {
async signIn(request: Request, options?: SignInOptions) {
return await signIn(request, oauthConfig, options);
},
- async handleCallback(request: Request) {
- return await handleCallback(request, oauthConfig, {
+ async handleCallback(
+ request: Request,
+ tokenHandler: (accessToken: string) => T | Promise,
+ ) {
+ return await handleCallback(request, oauthConfig, tokenHandler, {
cookieOptions: options?.cookieOptions,
});
},
async signOut(request: Request) {
return await signOut(request, { cookieOptions: options?.cookieOptions });
},
- async getSessionId(request: Request) {
- return await getSessionId(request, {
+ async getSessionData(request: Request) {
+ return await getSessionData(request, {
cookieName: options?.cookieOptions?.name,
});
},
diff --git a/lib/get_session_data.ts b/lib/get_session_data.ts
new file mode 100644
index 0000000..d75cbf2
--- /dev/null
+++ b/lib/get_session_data.ts
@@ -0,0 +1,42 @@
+// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
+import { getSessionIdCookie } from "./_http.ts";
+import { getSiteSession } from "./_kv.ts";
+
+/** Options for {@linkcode getSessionData}. */
+export interface GetSessionDataOptions {
+ /**
+ * The name of the cookie in the request. This must match the cookie name
+ * used in {@linkcode handleCallback} and {@linkcode signOut}.
+ */
+ cookieName?: string;
+}
+
+/**
+ * Gets the session ID from the cookie header of a request. This can be used to
+ * check whether the client is signed-in and whether the session ID was created
+ * on the server by checking if the return value is defined.
+ *
+ * @returns The session data object returned from {@linkcode sessionDataGetter}
+ * in {@linkcode handleCallback} or `null` if the session cookie either doesn't
+ * exist or the session entry doesn't exist in the database.
+ *
+ * @example
+ * ```ts
+ * import { getSessionData } from "https://deno.land/x/deno_kv_oauth/mod.ts";
+ *
+ * export async function handler(request: Request) {
+ * const sessionData = await getSessionData(request);
+ * return sessionData === null
+ * ? new Response("Unauthorized", { status: 401 })
+ * : Response.json(sessionData);
+ * }
+ * ```
+ */
+export async function getSessionData(
+ request: Request,
+ options?: GetSessionDataOptions,
+): Promise {
+ const sessionId = getSessionIdCookie(request, options?.cookieName);
+ if (sessionId === undefined) return null;
+ return await getSiteSession(sessionId);
+}
diff --git a/lib/get_session_id.ts b/lib/get_session_id.ts
deleted file mode 100644
index 6eed9aa..0000000
--- a/lib/get_session_id.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
-import { getSessionIdCookie } from "./_http.ts";
-import { isSiteSession } from "./_kv.ts";
-
-/** Options for {@linkcode getSessionId}. */
-export interface GetSessionIdOptions {
- /**
- * The name of the cookie in the request. This must match the cookie name
- * used in {@linkcode handleCallback} and {@linkcode signOut}.
- */
- cookieName?: string;
-}
-
-/**
- * Gets the session ID from the cookie header of a request. This can be used to
- * check whether the client is signed-in and whether the session ID was created
- * on the server by checking if the return value is defined.
- *
- * @example
- * ```ts
- * import { getSessionId } from "https://deno.land/x/deno_kv_oauth/mod.ts";
- *
- * export async function handler(request: Request) {
- * const sessionId = await getSessionId(request);
- * const hasSessionIdCookie = sessionId !== undefined;
- *
- * return Response.json({ sessionId, hasSessionIdCookie });
- * }
- * ```
- */
-export async function getSessionId(
- request: Request,
- options?: GetSessionIdOptions,
-): Promise {
- const sessionId = getSessionIdCookie(request, options?.cookieName);
- return (sessionId !== undefined && await isSiteSession(sessionId))
- ? sessionId
- : undefined;
-}
diff --git a/lib/get_session_id_test.ts b/lib/get_session_id_test.ts
deleted file mode 100644
index bd8051f..0000000
--- a/lib/get_session_id_test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
-import { assertEquals } from "std/assert/assert_equals.ts";
-import { SITE_COOKIE_NAME } from "./_http.ts";
-import { getSessionId } from "./get_session_id.ts";
-import { setSiteSession } from "./_kv.ts";
-
-Deno.test("getSessionId() returns undefined when cookie is not defined", async () => {
- const request = new Request("http://example.com");
-
- assertEquals(await getSessionId(request), undefined);
-});
-
-Deno.test("getSessionId() returns valid session ID", async () => {
- const sessionId = crypto.randomUUID();
- await setSiteSession(sessionId);
- const request = new Request("http://example.com", {
- headers: {
- cookie: `${SITE_COOKIE_NAME}=${sessionId}`,
- },
- });
-
- assertEquals(await getSessionId(request), sessionId);
-});
-
-Deno.test("getSessionId() returns valid session ID when cookie name is defined", async () => {
- const sessionId = crypto.randomUUID();
- await setSiteSession(sessionId);
- const cookieName = "triple-choc";
- const request = new Request("http://example.com", {
- headers: {
- cookie: `${cookieName}=${sessionId}`,
- },
- });
-
- assertEquals(await getSessionId(request, { cookieName }), sessionId);
-});
diff --git a/lib/handle_callback.ts b/lib/handle_callback.ts
index 3effc2c..19b8517 100644
--- a/lib/handle_callback.ts
+++ b/lib/handle_callback.ts
@@ -6,7 +6,6 @@ import {
type OAuth2ClientConfig,
SECOND,
setCookie,
- Tokens,
} from "../deps.ts";
import {
COOKIE_BASE,
@@ -19,8 +18,9 @@ import {
import { getAndDeleteOAuthSession, setSiteSession } from "./_kv.ts";
/** Options for {@linkcode handleCallback}. */
-export interface HandleCallbackOptions {
- /** Overwrites cookie properties set in the response. These must match the
+export interface HandleCallbackOptions {
+ /**
+ * Overwrites cookie properties set in the response. These must match the
* cookie properties used in {@linkcode getSessionId} and
* {@linkcode signOut}.
*/
@@ -32,33 +32,45 @@ export interface HandleCallbackOptions {
* then redirects the client to the success URL set in {@linkcode signIn}. The
* request URL must match the redirect URL of the OAuth application.
*
+ * @param tokenCallback Function that uses the access token to get the session
+ * object. This is used for fetching the user's profile from the OAuth
+ * provider. An {@linkcode Error} will be thrown if this function resolves to a
+ * `null` or `undefined` value. This is because `null` represents a non-existent
+ * session object and `undefined` is too similar, and may be confusing, from
+ * {@linkcode getSessionData}.
+ *
* @example
* ```ts
* import { handleCallback, createGitHubOAuthConfig } from "https://deno.land/x/deno_kv_oauth/mod.ts";
*
* const oauthConfig = createGitHubOAuthConfig();
*
+ * async function getGitHubUser(accessToken: string) {
+ * const response = await fetch("https://api.github.com/user", {
+ * headers: {
+ * Authorization: `bearer ${accessToken}`,
+ * },
+ * });
+ * if (!response.ok) throw new Error("Failed to fetch GitHub user profile");
+ * return await response.json();
+ * }
+ *
* export async function handleOAuthCallback(request: Request) {
- * const { response, tokens, sessionId } = await handleCallback(
+ * return await handleCallback(
* request,
* oauthConfig,
- * );
- *
- * // Perform some actions with the `tokens` and `sessionId`.
- *
- * return response;
+ * getGitHubUser,
+ * );
* }
* ```
*/
-export async function handleCallback(
+// deno-lint-ignore ban-types
+export async function handleCallback = {}>(
request: Request,
oauthConfig: OAuth2ClientConfig,
- options?: HandleCallbackOptions,
-): Promise<{
- response: Response;
- sessionId: string;
- tokens: Tokens;
-}> {
+ tokenCallback: (accessToken: string) => T | Promise,
+ options?: HandleCallbackOptions,
+): Promise {
const oauthCookieName = getCookieName(
OAUTH_COOKIE_NAME,
isHttps(request.url),
@@ -80,14 +92,17 @@ export async function handleCallback(
...options?.cookieOptions,
};
setCookie(response.headers, cookie);
+
+ const sessionData = await tokenCallback(tokens.accessToken);
+ if (sessionData === null || sessionData === undefined) {
+ throw new Error("tokenCallback() must resolve to a non-nullable value");
+ }
+
await setSiteSession(
sessionId,
+ sessionData,
cookie.maxAge ? cookie.maxAge * SECOND : undefined,
);
- return {
- response,
- sessionId,
- tokens,
- };
+ return response;
}
diff --git a/lib/handle_callback_test.ts b/lib/handle_callback_test.ts
index 3db7f5e..c16d191 100644
--- a/lib/handle_callback_test.ts
+++ b/lib/handle_callback_test.ts
@@ -1,8 +1,18 @@
// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
import { handleCallback } from "./handle_callback.ts";
-import { assertEquals, assertRejects } from "std/assert/mod.ts";
+import {
+ assert,
+ assertEquals,
+ assertNotEquals,
+ assertRejects,
+} from "std/assert/mod.ts";
+import { getSetCookies } from "std/http/cookie.ts";
import { returnsNext, stub } from "std/testing/mock.ts";
-import { getAndDeleteOAuthSession, setOAuthSession } from "./_kv.ts";
+import {
+ getAndDeleteOAuthSession,
+ getSiteSession,
+ setOAuthSession,
+} from "./_kv.ts";
import { OAUTH_COOKIE_NAME } from "./_http.ts";
import {
assertRedirect,
@@ -15,7 +25,7 @@ import type { Cookie } from "../deps.ts";
Deno.test("handleCallback() rejects for no OAuth cookie", async () => {
const request = new Request("http://example.com");
await assertRejects(
- async () => await handleCallback(request, randomOAuthConfig()),
+ async () => await handleCallback(request, randomOAuthConfig(), () => true),
Error,
"OAuth cookie not found",
);
@@ -26,7 +36,18 @@ Deno.test("handleCallback() rejects for non-existent OAuth session", async () =>
headers: { cookie: `${OAUTH_COOKIE_NAME}=xxx` },
});
await assertRejects(
- async () => await handleCallback(request, randomOAuthConfig()),
+ async () => await handleCallback(request, randomOAuthConfig(), () => true),
+ Deno.errors.NotFound,
+ "OAuth session not found",
+ );
+});
+
+Deno.test("handleCallback() rejects for non-existent OAuth session", async () => {
+ const request = new Request("http://example.com", {
+ headers: { cookie: `${OAUTH_COOKIE_NAME}=xxx` },
+ });
+ await assertRejects(
+ async () => await handleCallback(request, randomOAuthConfig(), () => true),
Deno.errors.NotFound,
"OAuth session not found",
);
@@ -39,7 +60,9 @@ Deno.test("handleCallback() deletes the OAuth session KV entry", async () => {
const request = new Request("http://example.com", {
headers: { cookie: `${OAUTH_COOKIE_NAME}=${oauthSessionId}` },
});
- await assertRejects(() => handleCallback(request, randomOAuthConfig()));
+ await assertRejects(async () =>
+ await handleCallback(request, randomOAuthConfig(), () => true)
+ );
await assertRejects(
async () => await getAndDeleteOAuthSession(oauthSessionId),
Deno.errors.NotFound,
@@ -67,19 +90,24 @@ Deno.test("handleCallback() correctly handles the callback response", async () =
const request = new Request(`http://example.com/callback?${searchParams}`, {
headers: { cookie: `${OAUTH_COOKIE_NAME}=${oauthSessionId}` },
});
- const { response, tokens, sessionId } = await handleCallback(
+ const response = await handleCallback(
request,
randomOAuthConfig(),
+ (accessToken) => accessToken,
);
fetchStub.restore();
assertRedirect(response, oauthSession.successUrl);
- assertEquals(
- response.headers.get("set-cookie"),
- `site-session=${sessionId}; HttpOnly; Max-Age=7776000; SameSite=Lax; Path=/`,
- );
- assertEquals(tokens.accessToken, tokensBody.access_token);
- assertEquals(typeof sessionId, "string");
+ const [cookie] = getSetCookies(response.headers);
+ assert(cookie !== undefined);
+ assertEquals(cookie.name, "site-session");
+ assertEquals(cookie.maxAge, 7776000);
+ assertEquals(cookie.sameSite, "Lax");
+ assertEquals(cookie.path, "/");
+ assertEquals(await getSiteSession(cookie.value), tokensBody.access_token);
+
+ const sessionId = cookie.value;
+ assertNotEquals(sessionId, undefined);
await assertRejects(
async () => await getAndDeleteOAuthSession(oauthSessionId),
Deno.errors.NotFound,
@@ -112,23 +140,91 @@ Deno.test("handleCallback() correctly handles the callback response with options
maxAge: 420,
domain: "example.com",
};
- const { response, tokens, sessionId } = await handleCallback(
+ const response = await handleCallback(
request,
randomOAuthConfig(),
+ (accessToken) => accessToken,
{ cookieOptions },
);
fetchStub.restore();
assertRedirect(response, oauthSession.successUrl);
- assertEquals(
- response.headers.get("set-cookie"),
- `${cookieOptions.name}=${sessionId}; HttpOnly; Max-Age=${cookieOptions.maxAge}; Domain=${cookieOptions.domain}; SameSite=Lax; Path=/`,
- );
- assertEquals(tokens.accessToken, tokensBody.access_token);
- assertEquals(typeof sessionId, "string");
+ const [cookie] = getSetCookies(response.headers);
+ assert(cookie !== undefined);
+ assertEquals(cookie.name, cookieOptions.name);
+ assertEquals(cookie.maxAge, cookieOptions.maxAge);
+ assertEquals(cookie.domain, cookieOptions.domain);
+ assertEquals(cookie.sameSite, "Lax");
+ assertEquals(cookie.path, "/");
+ assertEquals(await getSiteSession(cookie.value), tokensBody.access_token);
+
+ const sessionId = cookie.value;
+ assertNotEquals(sessionId, undefined);
await assertRejects(
async () => await getAndDeleteOAuthSession(oauthSessionId),
Deno.errors.NotFound,
"OAuth session not found",
);
});
+
+Deno.test("handleCallback() throws when `tokenCallback()` resolves to `undefined`", async () => {
+ const tokensBody = randomTokensBody();
+ const fetchStub = stub(
+ window,
+ "fetch",
+ returnsNext([Promise.resolve(Response.json(tokensBody))]),
+ );
+ const oauthSessionId = crypto.randomUUID();
+ const oauthSession = randomOAuthSession();
+ await setOAuthSession(oauthSessionId, oauthSession, { expireIn: 1_000 });
+ const searchParams = new URLSearchParams({
+ "response_type": "code",
+ "client_id": "clientId",
+ "code_challenge_method": "S256",
+ code: "code",
+ state: oauthSession.state,
+ });
+ const request = new Request(`http://example.com/callback?${searchParams}`, {
+ headers: { cookie: `${OAUTH_COOKIE_NAME}=${oauthSessionId}` },
+ });
+
+ await assertRejects(
+ async () =>
+ // @ts-ignore Just for the test
+ await handleCallback(request, randomOAuthConfig(), () => undefined),
+ Error,
+ "tokenCallback() must resolve to a non-nullable value",
+ );
+ fetchStub.restore();
+});
+
+Deno.test("handleCallback() throws when `tokenCallback()` resolves to `null`", async () => {
+ const tokensBody = randomTokensBody();
+ const fetchStub = stub(
+ window,
+ "fetch",
+ returnsNext([Promise.resolve(Response.json(tokensBody))]),
+ );
+ const oauthSessionId = crypto.randomUUID();
+ const oauthSession = randomOAuthSession();
+ await setOAuthSession(oauthSessionId, oauthSession, { expireIn: 1_000 });
+ const searchParams = new URLSearchParams({
+ "response_type": "code",
+ "client_id": "clientId",
+ "code_challenge_method": "S256",
+ code: "code",
+ state: oauthSession.state,
+ });
+ const request = new Request(`http://example.com/callback?${searchParams}`, {
+ headers: { cookie: `${OAUTH_COOKIE_NAME}=${oauthSessionId}` },
+ });
+
+ await assertRejects(
+ async () =>
+ // @ts-ignore Just for the test
+ await handleCallback(request, randomOAuthConfig(), () => null),
+ Error,
+ "tokenCallback() must resolve to a non-nullable value",
+ );
+ fetchStub.restore();
+});
diff --git a/lib/sign_out_test.ts b/lib/sign_out_test.ts
index 7183903..c077f05 100644
--- a/lib/sign_out_test.ts
+++ b/lib/sign_out_test.ts
@@ -14,7 +14,7 @@ Deno.test("signOut() returns a redirect response if the user is not signed-in",
Deno.test("signOut() returns a response that signs out the signed-in user", async () => {
const sessionId = crypto.randomUUID();
- await setSiteSession(sessionId);
+ await setSiteSession(sessionId, undefined);
const request = new Request("http://example.com/signout", {
headers: {
cookie: `${SITE_COOKIE_NAME}=${sessionId}`,
@@ -36,7 +36,7 @@ Deno.test("signOut() returns a response that signs out the signed-in user with c
path: "/path",
};
const sessionId = crypto.randomUUID();
- await setSiteSession(sessionId);
+ await setSiteSession(sessionId, undefined);
const request = new Request("http://example.com/signout", {
headers: {
cookie: `${cookieOptions.name}=${sessionId}`,
diff --git a/lib/types.ts b/lib/types.ts
index d602759..92932e2 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,2 +1,2 @@
// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
-export type { Cookie, OAuth2ClientConfig, Tokens } from "../deps.ts";
+export type { Cookie, OAuth2ClientConfig } from "../deps.ts";
diff --git a/mod.ts b/mod.ts
index 29c24b6..ddc7909 100644
--- a/mod.ts
+++ b/mod.ts
@@ -1,8 +1,5 @@
// Copyright 2023-2024 the Deno authors. All rights reserved. MIT license.
-export * from "./lib/handle_callback.ts";
-export * from "./lib/get_session_id.ts";
-export * from "./lib/sign_in.ts";
-export * from "./lib/sign_out.ts";
+
export * from "./lib/create_auth0_oauth_config.ts";
export * from "./lib/create_azure_ad_oauth_config.ts";
export * from "./lib/create_azure_adb2c_oauth_config.ts";
@@ -20,4 +17,8 @@ export * from "./lib/create_spotify_oauth_config.ts";
export * from "./lib/create_twitter_oauth_config.ts";
export * from "./lib/create_helpers.ts";
export * from "./lib/get_required_env.ts";
+export * from "./lib/handle_callback.ts";
+export * from "./lib/sign_in.ts";
+export * from "./lib/sign_out.ts";
+export * from "./lib/get_session_data.ts";
export * from "./lib/types.ts";