GithubHelp home page GithubHelp logo

jcs224 / oak_sessions Goto Github PK

View Code? Open in Web Editor NEW
30.0 3.0 7.0 396 KB

Session middleware for Oak

Home Page: https://deno.land/x/oak_sessions

License: MIT License

TypeScript 100.00%
hacktoberfest deno oak

oak_sessions's Introduction

Oak Sessions

Popularity Latest Version

Use cookie-based web sessions with the Oak framework. Supports flash messages - session data that is deleted after it's read.

Usage

import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { Session } from "https://deno.land/x/oak_sessions/mod.ts";

type AppState = {
    session: Session
}
const app = new Application<AppState>()

app.addEventListener('error', (evt) => {
    console.log(evt.error)
})

const router = new Router<AppState>();

// Apply sessions to your Oak application.
// You can also apply the middleware to specific routes instead of the whole app.
// Without params, default MemoryStore is used. See the Storage chapter below for more info.
app.use(Session.initMiddleware())

router.post('/login', async (ctx) => {
    const form = await ctx.request.body({type: 'form'}).value
    if(form.get('password') === 'correct') {
        // Set persistent data in the session
        ctx.state.session.set('email', form.get('email'))
        ctx.state.session.set('failed-login-attempts', null)
        // Set flash data in the session. This will be removed the first time it's accessed with get
        ctx.state.session.flash('message', 'Login successful')
    } else {
        const failedLoginAttempts = (await ctx.state.session.get('failed-login-attempts') || 0) as number
        ctx.state.session.set('failed-login-attempts', failedLoginAttempts+1)
        ctx.state.session.flash('error', 'Incorrect username or password')
    }
    ctx.response.redirect('/')
})

router.post('/logout', async (ctx) => {
    // Clear all session data
    await ctx.state.session.deleteSession()
    ctx.response.redirect('/')
})

router.get("/", async (ctx) => {
    const message = await ctx.state.session.get('message') || ''
    const error = await ctx.state.session.get('error') || ''
    const failedLoginAttempts = await ctx.state.session.get('failed-login-attempts')
    const email = await ctx.state.session.get('email')
    ctx.response.body = `<!DOCTYPE html>
    <body>
        <p>
            ${message}
        </p>
        <p>
            ${error}
        </p>
        <p>
            ${failedLoginAttempts ? `Failed login attempts: ${failedLoginAttempts}` : ''}
        </p>

        ${email ? 
        `<form id="logout" action="/logout" method="post">
            <button name="logout" type="submit">Log out ${email}</button>
        </form>`
        : 
        `<form id="login" action="/login" method="post">
            <p>
                <input id="email" name="email" type="text" placeholder="[email protected]">
            </p>
            <p>
                <input id="password" name="password" type="password" placeholder="password">
            </p>
            <button name="login" type="submit">Log in</button>
        </form>` 
    }
    </body>`;
})

app.use(router.routes());
app.use(router.allowedMethods());

app.listen({ port: 8002 });

Storage

You can specify the storage layer used to store session data. Here are the supported storage layers:

  • Memory: Stores all session data within memory. Good for debugging and testing, but should not be used in production.
  • Cookie: Stores all session data inside of an (optionally) encrypted cookie. The simplest implementation that doesn't require a backend and is suitable for production. The disadvantage is cookies can only store a pretty limited amount of data (about 4KB in most browsers) so only use if you don't need much session data.
  • SQLite: Uses a SQLite database to store session data. Internally, the deno sqlite library is used to interact with a SQLite database. Requires filesystem access.
  • Postgres: Uses a Postgres database to store session data. Internally, the deno postgres.js library is used to interact with a Postgres database. Requires a separate Postgres server.
  • Redis: Uses a Redis database to store session data. Internally, the deno redis library is used to interact with a Redis database. Requires a separate Redis server.
  • Mongo: Uses a Mongo database to store session data. Internally, the deno mongo library is used to interact with MongoDB. Requires a separate MongoDB server.
  • Webdis: Uses a Webdis endpoint to store session data. Webdis is a Redis server which allows you to use Redis with an HTTP endpoint. This is ideal for serverless environments, or anywhere that only HTTP endpoints can be accessed (such as Deno Deploy). Requires a Webdis URL.

By default, MemoryStorage is the storage driver, but you can (and should in production) use a more robust and persistent storage driver.

Cookie

import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { Session, CookieStore } from "https://deno.land/x/oak_sessions/mod.ts";

const app = new Application();
// cookie name for the store is configurable, default is: {sessionDataCookieName: 'session_data'}
const store = new CookieStore('very-secret-key')

// Attach sessions to middleware
app.use(Session.initMiddleware(store));

// ...

SQLite

import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { Session, SqliteStore } from "https://deno.land/x/oak_sessions/mod.ts";
import { DB } from 'https://deno.land/x/[email protected]/mod.ts'

const app = new Application();
const sqlite = new DB('./database.db') 
// Pass DB instance into a new SqliteStore. Optionally add a custom table name as second string argument, default is 'sessions'
const store = new SqliteStore(sqlite, 'optional_custom_table_name')

// Attach sessions to middleware. 
app.use(Session.initMiddleware(store))

// ...

Postgres

import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { Session, PostgresStore } from "https://deno.land/x/oak_sessions/mod.ts";
import postgres from 'https://deno.land/x/[email protected]/mod.js'

const app = new Application();

// Create a postgres connection, or use an existing one
const sql = postgres({
    host: 'localhost',
    port: 26257,
    database: 'defaultdb',
    user: 'root',
    password: '',
})

// Pass postgres connection into a new PostgresStore. Optionally add a custom table name as second string argument, default is 'sessions'
const store = new PostgresStore(sql, 'optional_custom_table_name')

// Initialize sessions table. Will create a table if one doesn't exist already.
await store.initSessionsTable()

// Attach sessions to middleware
app.use(Session.initMiddleware(store));

// ...

Redis

import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { Session, RedisStore } from "https://deno.land/x/oak_sessions/mod.ts";

// import the Redis library
import { connect } from 'https://deno.land/x/[email protected]/mod.ts'

const app = new Application();

// Create a redis connection
const redis = await connect({
    hostname: '0.0.0.0',
    port: 6379
})

// pass redis connection into a new RedisStore. Optionally add a second string argument for a custom database prefix, default is 'session_'
const store = new RedisStore(redis)

// Attach sessions to middleware
app.use(Session.initMiddleware(store));

// ...

Mongo

import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { Session, MongoStore } from "https://deno.land/x/oak_sessions/mod.ts";
import { MongoClient } from "https://deno.land/x/[email protected]/mod.ts";

const app = new Application();

// Create mongo connection or use an existing one
const client = new MongoClient();
const db = client.database('default');

// Pass mongo connection into a new MongoStore. Optionally add a custom collection name as second string argument, default is 'sessions'
const store = new MongoStore(db, 'optional_custom_collection_name');

// Attach sessions to middleware
app.use(Session.initMiddleware(store));

// ...

Webdis

import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { Session, WebdisStore } from "https://deno.land/x/oak_sessions/mod.ts";

const app = new Application();
const store = new WebdisStore({
    url: 'http://127.0.0.1:7379',
});

// Attach sessions to middleware
app.use(Session.initMiddleware(store));

// ...

More stores will be added over time.

Cookie

Whichever store you are using, a session id is requred to be saved in cookie so incoming requests can be identified. You can modified the options used when setting / deleting session id in cookie. Note that this option is different from options in CookieStore.

app.use(Session.initMiddleware(store, {
    cookieSetOptions: {
        httpOnly: true,
        sameSite: "none",
        secure: true
    },
    cookieGetOptions: {}
}))

Session key rotation

Sometimes, you'll want to rotate the session key to help prevent session fixation attacks. If you're using this library to authenticate users, it's wise to rotate the key immediately after the user logs in.

To rotate the session key, simply add an Oak context state variable on the appropriate route or middleware. The variable can be set before or after the session is initialized.

(ctx, next) => {
    ctx.state.rotate_session_key = true
}

โš ๏ธ Session key rotation isn't necessary with CookieStore, by nature of how storing all session data in a cookie works, instead of just a session ID. See the iron-session FAQ, which explains the reasoning very well.

Migrating from 3.x to 4.x

There are some breaking changes in how you initialize your session, but all of the ctx.state.session methods (get, set, flash, has) still work as they did before, except deleteSession no longer takes any arguments, which may or may not be breaking depending on how it's used in your project.

See more detail in the migration guide wiki.

oak_sessions's People

Contributors

aaronwlee avatar akatechis avatar anatolykopyl avatar aslakhellesoy avatar c0per avatar cmorten avatar denjucks avatar jcs224 avatar orangegrove1955 avatar reggi avatar snurppa avatar tamasszoke avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

oak_sessions's Issues

getting an error using denodb

// authMiddleware.ts

import { Session, SqliteStore } from "../../deps.ts";
import { db } from "../db/db.ts";

const store = new SqliteStore(db);

export const session = new Session(store);

Argument of type 'Database' is not assignable to parameter of type 'DB'.
Type 'Database' is missing the following properties from type 'DB': _wasm, _open, _statements, prepareQuery, and 5 more.deno-ts(2345)

https://github.com/kinghat/snapraid-gui/blob/2a9141ef149818e2659a1e14acaa038e28f56fd4/api/src/middlewares/authMiddleware.ts#L4

sorry for how messy it is. im switching over to sessions from tokens and ive got covid brain ๐Ÿค’ be gentle ๐Ÿ™

Bug: CookieStore shouldn't be shard between Context.

Unlink other stores in src/stores/, CookieStore only hold one SessionData. As in readme, CookieStore is defined outside the App and used in app.use(), so that one instance is shared between different contexts (different requests and responses)!

As a result, I'm seeing error like this:

TypeError: Cannot read properties of null (reading '_delete')
    at https://deno.land/x/oak_sessions@v3.5.3/src/Session.ts:83:34
    at async dispatch (https://deno.land/x/oak@v10.6.0/middleware.ts:41:7)
...

It looks like it's because another concurrent request arrived, session data (in CookieStore) gets cleared.
Or even worse, session A can sometimes get the data from session B when using cookieStore.

Main readme example doesn't work with Oak current

Hi, thanks for making this, it seems cool (and the Bedrock team selected it, so it must be good)!

I'm a not-so-good JS developer, but am experimenting with this.

I tried to use the Readme example with Deno, and got this error:

c@macmini ~/D/r/oak_sessions> deno run ./foo.ts
Check file:///Users/c/Documents/react/oak_sessions/foo.ts
error: TS2345 [ERROR]: Argument of type '(ctx: Context, next: () => Promise<unknown>) => Promise<void>' is not assignable to parameter of type 'Middleware<State, Context<State, AppState>>'.
  Types of parameters 'ctx' and 'context' are incompatible.
    Type 'Context<State, AppState>' is not assignable to type 'Context<State, Record<string, any>>'.
      Types of property 'app' are incompatible.
        Type 'Application<AppState>' is not assignable to type 'Application<Record<string, any>>'.
          Property '#contextState' in type 'Application' refers to a different member that cannot be accessed from within type 'Application'.
app.use(session.initMiddleware())
        ~~~~~~~~~~~~~~~~~~~~~~~~
    at file:///Users/c/Documents/react/oak_sessions/foo.ts:19:9

I had no idea what to do with that error.

Luckily, I found one of the Oak versions this was authored with in the file 'deps.ts'

By modifing the Readme example to use the same Oak version, the Readme example runs fine.

Maybe you want to change the Readme to include the Oak version, so it works for others out of the box?

Anyway, thanks for making & sharing this!

quick question

just making an issue for this question (i usually dont get notifs on comments on github): 3d7495b

Session time limit? Garbage collection?

I can't find any feature to limit the length of a session or to clean up old sessions from the database (mongodb in my case). Is there some recommended way to handle this?

Move `ctx.session` to `ctx.state.session` to correctly type ctx.

When I use app.use(session.initMiddleware());, I'm having this error:
image

It looks like that SessionContext type of ctx in session.initMiddleware() seems to be the problem.

Plus, I need to add property session to type Context to avoid some ts errors.

I think move ctx.session to ctx.state.session wil solve the problem just fine.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.