@withstudiocms/auth-kit
Version:
Utilities for managing authentication
153 lines (152 loc) • 5.21 kB
JavaScript
import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding";
import { Effect } from "@withstudiocms/effect";
import { SessionError, useSessionError, useSessionErrorPromise } from "../errors.js";
import { defaultSessionConfig, makeExpirationDate, makeSessionId } from "../utils/session.js";
const Session = (config) => Effect.gen(function* () {
const { expTime, cookieName, sessionTools } = {
...defaultSessionConfig,
...config
};
if (!sessionTools) {
return yield* Effect.fail(
new SessionError({ cause: "Session tools must be provided in the configuration" })
);
}
if (!Number.isFinite(expTime) || expTime <= 0) {
return yield* Effect.fail(
new SessionError({
cause: "Invalid session config: expTime must be a positive number (ms)"
})
);
}
if (typeof cookieName !== "string" || cookieName.trim().length === 0) {
return yield* Effect.fail(
new SessionError({ cause: "Invalid session config: cookieName must be a non-empty string" })
);
}
const expTimeHalf = expTime / 2;
const defaultSecure = process.env.NODE_ENV === "production";
const generateSessionToken = Effect.fn(
"@withstudiocms/AuthKit/modules/session.generateSessionToken"
)(
() => useSessionError(() => {
const data = new Uint8Array(20);
const random = crypto.getRandomValues(data);
const returnable = encodeBase32LowerCaseNoPadding(random);
return returnable;
})
);
const createSession = Effect.fn("@withstudiocms/AuthKit/modules/session.createSession")(
function* (token, userId) {
const [sessionId, expirationDate] = yield* Effect.all([
makeSessionId(token),
makeExpirationDate(expTime)
]);
return yield* useSessionErrorPromise(
() => sessionTools.createSession({ expiresAt: expirationDate, id: sessionId, userId })
);
}
);
const validateSessionToken = Effect.fn(
"@withstudiocms/AuthKit/modules/session.validateSessionToken"
)(function* (token) {
const sessionId = yield* makeSessionId(token);
const nullSession = {
session: null,
user: null
};
const result = yield* useSessionErrorPromise(
() => sessionTools.sessionAndUserData(sessionId)
);
if (!result.length) {
return nullSession;
}
const userSession = result[0];
if (!userSession) {
return nullSession;
}
const { user, session } = userSession;
const now = Date.now();
const expiresAtMs = session.expiresAt instanceof Date ? session.expiresAt.getTime() : new Date(session.expiresAt).getTime();
if (!Number.isFinite(expiresAtMs)) {
yield* useSessionErrorPromise(() => sessionTools.deleteSession(session.id));
return nullSession;
}
if (now >= expiresAtMs) {
yield* useSessionErrorPromise(() => sessionTools.deleteSession(session.id));
return nullSession;
}
let sessionOut = session;
if (now >= expiresAtMs - expTimeHalf) {
const expiresAt = new Date(now + expTime);
yield* useSessionErrorPromise(
() => sessionTools.updateSession(session.id, { ...session, expiresAt })
);
sessionOut = { ...session, expiresAt };
}
return { session: sessionOut, user };
});
const invalidateSession = Effect.fn("@withstudiocms/AuthKit/modules/session.invalidateSession")(
(sessionId) => useSessionErrorPromise(() => sessionTools.deleteSession(sessionId))
);
const setSessionTokenCookie = Effect.fn(
"@withstudiocms/AuthKit/modules/session.setSessionTokenCookie"
)(
(context, token, expiresAt, secure) => useSessionError(
() => context.cookies.set(cookieName, token, {
httpOnly: true,
sameSite: "lax",
secure: secure ?? defaultSecure,
expires: expiresAt,
path: "/"
})
)
);
const deleteSessionTokenCookie = Effect.fn(
"@withstudiocms/AuthKit/modules/session.deleteSessionTokenCookie"
)(
(context, secure) => useSessionError(
() => context.cookies.set(cookieName, "", {
httpOnly: true,
sameSite: "lax",
secure: secure ?? defaultSecure,
maxAge: 0,
path: "/"
})
)
);
const setOAuthSessionTokenCookie = Effect.fn(
"@withstudiocms/AuthKit/modules/session.setOAuthSessionTokenCookie"
)(
(context, key, value, secure) => useSessionError(
() => context.cookies.set(key, value, {
httpOnly: true,
sameSite: "lax",
secure: secure ?? defaultSecure,
maxAge: 60 * 10,
path: "/"
})
)
);
const createUserSession = Effect.fn("@withstudiocms/AuthKit/modules/session.createUserSession")(
function* (userId, context, secure) {
const sessionToken = yield* generateSessionToken();
const expirationDate = yield* makeExpirationDate(expTime);
yield* createSession(sessionToken, userId);
yield* setSessionTokenCookie(context, sessionToken, expirationDate, secure);
}
);
return {
generateSessionToken,
createSession,
validateSessionToken,
invalidateSession,
setSessionTokenCookie,
deleteSessionTokenCookie,
setOAuthSessionTokenCookie,
createUserSession
};
});
export {
Session
};